diff --git a/DESCRIPTION b/DESCRIPTION index 6d618e3..c822767 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,12 +17,12 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 Imports: - methods, - utils + lubridate Suggests: knitr, rmarkdown, ncdf4, + ncdfCF, RNetCDF, testthat (>= 3.0.0), stringr @@ -30,3 +30,17 @@ URL: https://github.com/pvanlaake/CFtime BugReports: https://github.com/pvanlaake/CFtime/issues VignetteBuilder: knitr Config/testthat/edition: 3 +Collate: + 'api.R' + 'CFCalendar.R' + 'CFCalendar360.R' + 'CFCalendar365.R' + 'CFCalendar366.R' + 'CFCalendarJulian.R' + 'CFCalendarProleptic.R' + 'CFCalendarStandard.R' + 'CFtime-package.R' + 'CFtime.R' + 'deprecated.R' + 'helpers.R' + 'zzz.R' diff --git a/NAMESPACE b/NAMESPACE index fc5cf62..67da2a2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,35 +1,38 @@ # Generated by roxygen2: do not edit by hand -S3method(str,CFdatum) +S3method("+",CFTime) +S3method("==",CFTime) +S3method(as.character,CFTime) +S3method(cut,CFTime) +S3method(length,CFTime) +S3method(range,CFTime) +export("bounds<-") +export(CFTime) export(CFcomplete) -export(CFdatum) export(CFfactor) export(CFfactor_coverage) export(CFfactor_units) export(CFmonth_days) export(CFparse) +export(CFsubset) export(CFtime) export(CFtimestamp) export(as_timestamp) +export(bounds) export(calendar) export(definition) +export(indexOf) export(is_complete) export(month_days) export(offsets) export(origin) +export(parse_timestamps) export(resolution) export(slab) export(timezone) export(unit) -exportClasses(CFdatum) -exportClasses(CFtime) -exportMethods("+") -exportMethods("==") -exportMethods("bounds<-") -exportMethods(as.character) -exportMethods(bounds) -exportMethods(cut) -exportMethods(format) -exportMethods(indexOf) -exportMethods(length) -exportMethods(range) +importFrom(lubridate,days) +importFrom(lubridate,make_date) +importFrom(lubridate,mday) +importFrom(lubridate,month) +importFrom(lubridate,year) diff --git a/NEWS.md b/NEWS.md index e18a6a4..6d09653 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,14 @@ * Do not drop degenerate dimension on bounds when only 1 offset is included in subsetting. +* `standard` calendar now uses mixed Gregorian/Julian calendar as defined in the +CF Metadata Conventions. `proleptic_gregorian` is now a separate calendar with +its own code base. +* Negative offsets from a calendar origin are allowed. +* Code is refactored to R6. R6 class CFTime replaces S4 class CFtime (note the +difference in case). S4 class CFdatum has been replaced by hierarchy of +R6 CFCalendar classes, with various non-exported functions converted into +methods of CFCalendar. The code is now much cleaner and easier to extend. # CFtime 1.4.1 diff --git a/R/CFCalendar.R b/R/CFCalendar.R new file mode 100644 index 0000000..8ed2e09 --- /dev/null +++ b/R/CFCalendar.R @@ -0,0 +1,350 @@ +#' @title Basic CF calendar +#' +#' @description This class represents a basic CF calendar. It should not be +#' instantiated directly; instead, use one of the descendant classes. +#' +#' This internal class stores the information to represent date and time +#' values using the CF conventions. An instance is created by the exported +#' [CFTime] class, which also exposes the relevant properties of this class. +#' +#' The following calendars are supported: +#' +#' \itemize{ +#' \item [`gregorian\standard`][CFCalendarStandard], the international standard calendar for civil use. +#' \item [`proleptic_gregorian`][CFCalendarProleptic], the standard calendar but extending before 1582-10-15 +#' when the Gregorian calendar was adopted. +#' \item [`julian`][CFCalendarJulian], every fourth year is a leap year (so including the years 1700, 1800, 1900, 2100, etc). +#' \item [`noleap\365_day`][CFCalendar365], all years have 365 days. +#' \item [`all_leap\366_day`][CFCalendar366], all years have 366 days. +#' \item [`360_day`][CFCalendar360], all years have 360 days, divided over 12 months of 30 days. +#' } +#' @references +#' https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#calendar +#' @docType class +CFCalendar <- R6::R6Class("CFCalendar", + public = list( + #' @field name Descriptive name of the calendar, as per the CF Metadata + #' Conventions. + name = "", + + #' @field definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + definition = "", + + #' @field unit The numeric id of the unit of the calendar. + unit = -1L, + + #' @field origin `data.frame` with fields for the origin of the calendar. + origin = data.frame(), + + #' @description Create a new CF calendar. + #' @param nm The name of the calendar. This must follow the CF Metadata + #' Conventions. + #' @param definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + initialize = function(nm, definition) { + stopifnot(length(definition) == 1L, length(nm) == 1L) + self$name <- tolower(nm) + self$definition <- definition + + parts <- strsplit(definition, " ")[[1L]] + if ((length(parts) < 3L) || !(tolower(parts[2L]) %in% c("since", "after", "from", "ref", "per"))) + stop("Definition string does not appear to be a CF-compliant time coordinate description", call. = FALSE) + u <- which(CFt$CFunits$unit == tolower(parts[1L])) + if (length(u) == 0L) stop("Unsupported unit: ", parts[1L], call. = FALSE) + self$unit <- CFt$CFunits$id[u] + + dt <- self$parse(paste(parts[3L:length(parts)], collapse = " ")) + if (is.na(dt$year[1L])) + stop("Definition string does not appear to be a CF-compliant time coordinate description: invalid base date specification", call. = FALSE) + self$origin <- dt + }, + + #' @description Print information about the calendar to the console. + #' @param ... Ignored. + #' @return `self`, invisibly. + print = function(...) { + tz <- self$timezone + if (tz == "+0000") tz <- "" + cat("CF calendar:", + "\n Origin : ", self$origin_date, " ", self$origin_time, tz, + "\n Units : ", CFt$units$name[self$unit], + "\n Type : ", self$name, "\n", + sep = "") + invisible(self) + }, + + #' @description Indicate which of the supplied dates are valid. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return `NULL`. A warning will be generated to the effect that a + #' descendant class should be used for this method. + valid_days = function(ymd) { + warning("Use a descendant class from `CFCalendar` to call this method.", call. = FALSE) + NULL + }, + + #' @description Indicate if the time series described using this calendar + #' can be safely converted to a standard date-time type (`POSIXct`, + #' `POSIXlt`, `Date`). + #' + #' Only the 'standard' calendar and the 'proleptic_gregorian' calendar + #' when all dates in the time series are more recent than 1582-10-15 + #' (inclusive) can be safely converted, so this method returns `FALSE` by + #' default to cover the majority of cases. + #' @param offsets The offsets from the CFtime instance. + #' @return `FALSE` by default. + POSIX_compatible = function(offsets) { + FALSE + }, + + #' @description This method tests if the `CFCalendar` instance in argument + #' `cal` is compatible with `self`, meaning that they are of the same + #' class and have the same unit. Calendars "standard", and "gregorian" are + #' compatible, as are the pairs of "365_day" and "no_leap", and "366_day" + #' and "all_leap". + #' @param cal Instance of a descendant of the `CFCalendar` class. + #' @return `TRUE` if the instance in argument `cal` is compatible with + #' `self`, `FALSE` otherwise. + is_compatible = function(cal) { + self$unit == cal$unit && class(self)[1L] == class(cal)[1L] + }, + + #' @description This method tests if the `CFCalendar` instance in argument + #' `cal` is equivalent to `self`, meaning that they are of the same class, + #' have the same unit, and equivalent origins. Calendars "standard", and + #' "gregorian" are equivalent, as are the pairs of "365_day" and + #' "no_leap", and "366_day" and "all_leap". + #' + #' Note that the origins need not be identical, but their parsed values + #' have to be. "2000-01" is parsed the same as "2000-01-01 00:00:00", for + #' instance. + #' @param cal Instance of a descendant of the `CFCalendar` class. + #' @return `TRUE` if the instance in argument `cal` is equivalent to + #' `self`, `FALSE` otherwise. + is_equivalent = function(cal) { + sum(self$origin[1L,1L:6L] == cal$origin[1L,1L:6L]) == 6L && # Offset column is NA + self$is_compatible(cal) + }, + + #' @description Parsing a vector of date-time character strings into parts. + #' @param d character. A character vector of date-times. + #' @return A `data.frame` with columns year, month, day, hour, minute, + #' second, time zone, and offset. Invalid input data will appear as `NA`. + parse = function(d) { + # Parsers + + # UDUNITS broken timestamp definition, with some changes + # broken_timestamp {broken_date}({space|T}+{broken_clock})? -- T not in definition but present in lexer code + # broken_date {year}-{month}(-{day})? + # year [+-]?[0-9]{1,4} + # month 0?[1-9]|1[0-2] + # day 0?[1-9]|[1-2][0-9]|30|31 + # broken_clock {hour}:{minute}(:{second})? + # hour [0-1]?[0-9]|2[0-3] -- sign on hour not allowed, but see timezone + # minute [0-5]?[0-9] + # second {minute}? -- leap second not supported + # fractional part (\.[0-9]*)? + # timezone [+-]?{hour}(:{minute})? -- added, present in lexer code + broken <- paste0( + "^", # anchor string at start + "([+-]?[0-9]{1,4})", # year, with optional sign + "-(0?[1-9]|1[012])", # month + "(?:-(0?[1-9]|[12][0-9]|3[01]))?", # day, optional + "(?:[T ]", # if a time is following, separate with a single whitespace character or a "T" + "([01]?[0-9]|2[0-3])", # hour + ":([0-5]?[0-9])", # minute + "(?::([0-5]?[0-9]))?", # second, optional + "(?:\\.([0-9]*))?", # optional fractional part of the smallest specified unit + ")?", # close optional time capture group + "(?:\\s", # if a time zone offset is following, separate with a single whitespace character + "([+-])?([01]?[0-9]|2[0-3])", # tz hour, with optional sign + "(?::(00|15|30|45))?", # optional tz minute, only 4 possible values + ")?", # close optional timezone capture group + "$" # anchor string at end + ) + + iso8601 <- paste0( + "^", + "([0-9]{4})", + "-(0[1-9]|1[012])", + "-(0[1-9]|[12][0-9]|3[01])?", + "(?:", + "[T ]([01][0-9]|2[0-3])", + "(?::([0-5][0-9]))?", + "(?::([0-5][0-9]))?", + "(?:\\.([0-9]*))?", + ")?", + "(?:([Z+-])([01][0-9]|2[0-3])?(?::(00|15|30|45))?", ## FIXME: Z?, smaller number of captures + ")?$" + ) + + # UDUNITS packed timestamp definition - NOT YET USED + # packed_timestamp {packed_date}({space|T}+{packed_clock})? -- T and space only allowed in packed time follows + # packed_date {year}({month}{day}?)? -- must be YYYYMMDD or else format is ambiguous, as per lexer code + # packed_clock {hour}({minute}{second}?)? -- must be HHMMSS to be unambiguous + # timezone [+-]?{hour}({minute})? -- added, present in lexer code, must be HHMM + # packed <- stringi::stri_join( + # "^", # anchor string at start + # "([+-]?[0-9]{4})", # year, with optional sign + # "(0[1-9]|1[012])?", # month, optional + # "(0[1-9]|[12][0-9]|3[01])?", # day, optional + # "(?:[T,\\s]", # if a time is following, separate with a single whitespace character or a "T" + # "([01][0-9]|2[0-3])?", # hour + # "([0-5][0-9])?", # minute, optional + # "([0-5]?[0-9](?:\\.[0-9]*)?)?", # second, optional, with optional fractional part + # ")?", # close optional time capture group + # "(?:\\s", # if a time zone offset is following, separate with a single whitespace character + # "([+-]?[01][0-9]|2[0-3])?", # hour, with optional sign + # "(00|15|30|45)?", # minute, only 4 possible values + # ")?", # close optional timezone capture group + # "$" # anchor string at end + # ) + + parse <- data.frame(year = integer(), month = integer(), day = integer(), + hour = integer(), minute = integer(), second = numeric(), frac = character(), + tz_sign = character(), tz_hour = character(), tz_min = character()) + + # Drop "UTC", if given + d <- trimws(gsub("UTC$", "", d)) + + cap <- utils::strcapture(iso8601, d, parse) + missing <- which(is.na(cap$year)) + if (length(missing) > 0L) + cap[missing,] <- utils::strcapture(broken, d[missing], parse) + + # Assign any fraction to the appropriate time part + cap$frac[is.na(cap$frac)] <- "0" + frac <- as.numeric(paste0("0.", cap$frac)) + if (sum(frac) > 0) { + ndx <- which(!(is.na(cap$second)) & frac > 0) + if (length(ndx) > 0L) cap$second[ndx] <- cap$second[ndx] + frac[ndx] + ndx <- which(!(is.na(cap$minute)) & is.na(cap$second) & frac > 0) + if (length(ndx) > 0L) cap$second[ndx] <- 60L * frac[ndx] + ndx <- which(!(is.na(cap$hour)) & is.na(cap$minute) & frac > 0) + if (length(ndx) > 0L) { + secs <- 3600 * frac + cap$minute[ndx] <- secs[ndx] %/% 60 + cap$second[ndx] <- secs[ndx] %% 60 + } + } + cap$frac <- NULL + + # Convert NA time parts to 0 - in CF default time is 00:00:00 when not specified + cap$hour[is.na(cap$hour)] <- 0L + cap$minute[is.na(cap$minute)] <- 0L + cap$second[is.na(cap$second)] <- 0L + + # Set timezone to default value where needed + ndx <- which(cap$tz_sign == "Z") + if (length(ndx) > 0L) { + cap$tz_sign[ndx] <- "+" + cap$tz_hour[ndx] <- "00" + cap$tz_min[ndx] <- "00" + } + cap$tz <- paste0(ifelse(cap$tz_sign == "", "+", cap$tz_sign), + ifelse(cap$tz_hour == "", "00", cap$tz_hour), + ifelse(cap$tz_min == "", "00", cap$tz_min)) + cap$tz_sign <- cap$tz_hour <- cap$tz_min <- NULL + + # Set optional date parts to 1 if not specified + cap$month[is.na(cap$month)] <- 1L + cap$day[is.na(cap$day)] <- 1L + + # Check date validity + invalid <- !self$valid_days(cap) + if (sum(invalid, na.rm = TRUE) > 0L) cap[invalid,] <- rep(NA, 7) + + # Calculate offsets + if (nrow(self$origin) == 0L) { # if there's no origin yet, don't calculate offsets + cap$offset <- rep(0, nrow(cap)) # this happens, f.i., when a CFCalendar is created + } else { + days <- self$date2offset(cap) + cap$offset <- round((days * 86400 + (cap$hour - self$origin$hour[1]) * 3600 + + (cap$minute - self$origin$minute[1]) * 60 + + cap$second - self$origin$second) / CFt$units$seconds[self$unit], 3) + } + cap + }, + + #' @description Decompose a vector of offsets, in units of the calendar, to + #' their timestamp values. This adds a specified amount of time to the + #' origin of a `CFTime` object. + #' + #' This method may introduce inaccuracies where the calendar unit is + #' "months" or "years", due to the ambiguous definition of these units. + #' @param offsets Vector of numeric offsets to add to the origin of the + #' calendar. + #' @return A `data.frame` with columns for the timestamp elements and as + #' many rows as there are offsets. + offsets2time = function(offsets) { + len <- length(offsets) + if(len == 0L) return(data.frame(year = integer(), month = integer(), day = integer(), + hour = integer(), minute = integer(), second = numeric(), + tz = character(), offset = numeric())) + + if (self$unit <= 4L) { # Days, hours, minutes, seconds + # First add time: convert to seconds first, then recompute time parts + secs <- offsets * CFt$units$seconds[self$unit] + + self$origin$hour * 3600L + self$origin$minute * 60L + self$origin$second + days <- secs %/% 86400L # overflow days + secs <- round(secs %% 86400L, 3L) # drop overflow days from time, round down to milli-seconds to avoid errors + + # Time elements for output + hrs <- secs %/% 3600L + mins <- (secs %% 3600L) %/% 60L + secs <- secs %% 60L + + # Now add days using the calendar + out <- if (any(days != 0L)) + self$offset2date(days) + else + data.frame(year = rep(self$origin$year, len), + month = rep(self$origin$month, len), + day = rep(self$origin$day, len)) + + # Put it all back together again + out$hour <- hrs + out$minute <- mins + out$second <- secs + out$tz <- rep(self$timezone, len) + } else { # Months, years + out <- self$origin[rep(1L, len), ] + if (self$unit == 5L) { # Offsets are months + months <- out$month + offsets - 1L + out$month <- months %% 12L + 1L + out$year <- out$year + months %/% 12L + } else { # Offsets are years + out$year <- out$year + offsets + } + } + out$offset <- offsets + out + } + + ), + active = list( + #' @field origin_date (read-only) Character string with the date of the + #' calendar. + origin_date = function(value) { + if (missing(value)) { + sprintf("%04d-%02d-%02d", self$origin$year, self$origin$month, self$origin$day) + } + }, + + #' @field origin_time (read-only) Character string with the time of the + #' calendar. + origin_time = function(value) { + if (missing(value)) { + .format_time(self$origin) + } + }, + + #' @field timezone (read-only) Character string with the time zone of the + #' origin of the calendar. + timezone = function(value) { + if (missing(value)) + self$origin$tz + } + ) +) diff --git a/R/CFCalendar360.R b/R/CFCalendar360.R new file mode 100644 index 0000000..c73bd3a --- /dev/null +++ b/R/CFCalendar360.R @@ -0,0 +1,93 @@ +#' @title 360-day CF calendar +#' +#' @description This class represents a CF calendar of 360 days per year, evenly +#' divided over 12 months of 30 days. This calendar is obviously not +#' compatible with the standard POSIXt calendar. +#' +#' This calendar supports dates before year 1 and includes the year 0. +#' +#' @aliases CFCalendar360 +#' @docType class +CFCalendar360 <- R6::R6Class("CFCalendar360", + inherit = CFCalendar, + public = list( + #' @description Create a new CF calendar. + #' @param nm The name of the calendar. This must be "360_day". This argument + #' is superfluous but maintained to be consistent with the initialization + #' methods of the parent and sibling classes. + #' @param definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + #' @return A new instance of this class. + initialize = function(nm, definition) { + super$initialize(nm, definition) + }, + + #' @description Indicate which of the supplied dates are valid. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return Logical vector with the same length as argument `ymd` has rows + #' with `TRUE` for valid days and `FALSE` for invalid days, or `NA` where + #' the row in argument `ymd` has `NA` values. + valid_days = function(ymd) { + ymd$year & ymd$month >= 1L & ymd$month <= 12L & ymd$day >= 1L & ymd$day <= 30L + }, + + #' @description Determine the number of days in the month of the calendar. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return A vector indicating the number of days in each month for the + #' dates supplied as argument `ymd`. If no dates are supplied, the number + #' of days per month for the calendar as a vector of length 12. + month_days = function(ymd = NULL) { + if (is.null(ymd)) return(rep(30L, 12L)) + + res <- rep(30L, nrow(ymd)) + res[which(is.na(ymd$year))] <- NA + res + }, + + #' @description Indicate which years are leap years. + #' @param yr Integer vector of years to test. + #' @return Logical vector with the same length as argument `yr`. Since this + #' calendar does not use leap days, all values will be `FALSE`, or `NA` + #' where argument `yr` is `NA`. + leap_year = function(yr) { + res <- rep(FALSE, length(yr)) + res[which(is.na(yr))] <- NA + res + }, + + #' @description Calculate difference in days between a `data.frame` of time + #' parts and the origin. + #' + #' @param x `data.frame`. Dates to calculate the difference for. + #' + #' @return Integer vector of a length equal to the number of rows in + #' argument `x` indicating the number of days between `x` and the `origin`, + #' or `NA` for rows in `x` with `NA` values. + date2offset = function(x) { + (x$year - self$origin$year) * 360L + (x$month - self$origin$month) * 30L + x$day - self$origin$day + }, + + #' @description Calculate date parts from day differences from the origin. + #' This only deals with days as these are impacted by the calendar. + #' Hour-minute-second timestamp parts are handled in [CFCalendar]. + #' + #' @param x Integer vector of days to add to the origin. + #' + #' @return A `data.frame` with columns 'year', 'month' and 'day' and as many + #' rows as the length of vector `x`. + offset2date = function(x) { + y <- self$origin$year + x %/% 360L + m <- self$origin$month + (x %% 360L) %/% 30L + d <- self$origin$day + x %% 30L + over <- which(d > 30L) + d[over] <- d[over] - 30L + m[over] <- m[over] + 1L + over <- which(m > 12L) + m[over] <- m[over] - 12L + y[over] <- y[over] + 1L + data.frame(year = y, month = m, day = d, row.names = NULL) + } + ) +) diff --git a/R/CFCalendar365.R b/R/CFCalendar365.R new file mode 100644 index 0000000..8101ede --- /dev/null +++ b/R/CFCalendar365.R @@ -0,0 +1,101 @@ +#' @title 365-day CF calendar +#' +#' @description This class represents a CF calendar of 365 days per year, having +#' no leap days in any year. This calendar is not compatible with the standard +#' POSIXt calendar. +#' +#' This calendar supports dates before year 1 and includes the year 0. +#' +#' @aliases CFCalendar365 +#' @docType class +CFCalendar365 <- R6::R6Class("CFCalendar365", + inherit = CFCalendar, + public = list( + #' @description Create a new CF calendar of 365 days per year. + #' @param nm The name of the calendar. This must be "365_day" or "noleap". + #' @param definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + #' @return A new instance of this class. + initialize = function(nm, definition) { + super$initialize(nm, definition) + }, + + #' @description Indicate which of the supplied dates are valid. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return Logical vector with the same length as argument `ymd` has rows + #' with `TRUE` for valid days and `FALSE` for invalid days, or `NA` where + #' the row in argument `ymd` has `NA` values. + valid_days = function(ymd) { + ymd$year & ymd$month >= 1L & ymd$month <= 12L & ymd$day >= 1L & + ymd$day <= c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month] + }, + + #' @description Determine the number of days in the month of the calendar. + #' @param ymd `data.frame`, optional, with dates parsed into their parts. + #' @return A vector indicating the number of days in each month for the + #' dates supplied as argument `ymd`. If no dates are supplied, the number + #' of days per month for the calendar as a vector of length 12. + month_days = function(ymd = NULL) { + if (is.null(ymd)) return(c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)) + + res <- c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month] + res[which(is.na(ymd$year))] <- NA + res + }, + + #' @description Indicate which years are leap years. + #' @param yr Integer vector of years to test. + #' @return Logical vector with the same length as argument `yr`. Since this + #' calendar does not use leap days, all values will be `FALSE`, or `NA` + #' where argument `yr` is `NA`. + leap_year = function(yr) { + res <- rep(FALSE, length(yr)) + res[which(is.na(yr))] <- NA + res + }, + + #' @description Calculate difference in days between a `data.frame` of time + #' parts and the origin. + #' + #' @param x `data.frame`. Dates to calculate the difference for. + #' + #' @return Integer vector of a length equal to the number of rows in + #' argument `x` indicating the number of days between `x` and the `origin`, + #' or `NA` for rows in `x` with `NA` values. + date2offset = function(x) { + yd0 <- c(0L, 31L, 59L, 90L, 120L, 151L, 181L, 212L, 243L, 273L, 304L, 334L) # days diff of 1st of month to 1 January + (x$year - self$origin$year) * 365L + yd0[x$month] - yd0[self$origin$month] + x$day - self$origin$day + }, + + #' @description Calculate date parts from day differences from the origin. This + #' only deals with days as these are impacted by the calendar. + #' Hour-minute-second timestamp parts are handled in [CFCalendar]. + #' + #' @param x Integer vector of days to add to the origin. + #' @return A `data.frame` with columns 'year', 'month' and 'day' and as many + #' rows as the length of vector `x`. + offset2date = function(x) { + month <- c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L) + + # First process full years over the vector + yr <- self$origin$year + (x %/% 365L) + x <- x %% 365L + + # Remaining portion relative to the origin + x <- x + self$origin$day + ymd <- mapply(function(y, m, d) { + while (d > month[m]) { + d <- d - month[m] + m <- m + 1L + if (m == 13L) { + y <- y + 1L + m <- 1L + } + } + return(c(y, m, d)) + }, yr, self$origin$month, x) + data.frame(year = ymd[1L,], month = ymd[2L,], day = ymd[3L,], row.names = NULL) + } + ) +) diff --git a/R/CFCalendar366.R b/R/CFCalendar366.R new file mode 100644 index 0000000..4db2853 --- /dev/null +++ b/R/CFCalendar366.R @@ -0,0 +1,101 @@ +#' @title 366-day CF calendar +#' +#' @description This class represents a CF calendar of 366 days per year, having +#' leap days in every year. This calendar is not compatible with the standard +#' POSIXt calendar. +#' +#' This calendar supports dates before year 1 and includes the year 0. +#' +#' @aliases CFCalendar366 +#' @docType class +CFCalendar366 <- R6::R6Class("CFCalendar366", + inherit = CFCalendar, + public = list( + #' @description Create a new CF calendar of 366 days per year. + #' @param nm The name of the calendar. This must be "366_day" or "all_leap". + #' @param definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + #' @return A new instance of this class. + initialize = function(nm, definition) { + super$initialize(nm, definition) + }, + + #' @description Indicate which of the supplied dates are valid. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return Logical vector with the same length as argument `ymd` has rows + #' with `TRUE` for valid days and `FALSE` for invalid days, or `NA` where + #' the row in argument `ymd` has `NA` values. + valid_days = function(ymd) { + ymd$year & ymd$month >= 1L & ymd$month <= 12L & ymd$day >= 1L & + ymd$day <= c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month] + }, + + #' @description Determine the number of days in the month of the calendar. + #' @param ymd `data.frame`, optional, with dates parsed into their parts. + #' @return A vector indicating the number of days in each month for the + #' dates supplied as argument `ymd`. If no dates are supplied, the number + #' of days per month for the calendar as a vector of length 12. + month_days = function(ymd = NULL) { + if (is.null(ymd)) return(c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)) + + res <- c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month] + res[which(is.na(ymd$year))] <- NA + res + }, + + #' @description Indicate which years are leap years. + #' @param yr Integer vector of years to test. + #' @return Logical vector with the same length as argument `yr`. Since in + #' this calendar all years have a leap day, all values will be `TRUE`, or + #' `NA` where argument `yr` is `NA`. + leap_year = function(yr) { + res <- rep(TRUE, length(yr)) + res[which(is.na(yr))] <- NA + res + }, + + #' @description Calculate difference in days between a `data.frame` of time + #' parts and the origin. + #' + #' @param x `data.frame`. Dates to calculate the difference for. + #' + #' @return Integer vector of a length equal to the number of rows in + #' argument `x` indicating the number of days between `x` and the `origin`, + #' or `NA` for rows in `x` with `NA` values. + date2offset = function(x) { + yd0 <- c(0L, 31L, 60L, 91L, 121L, 152L, 182L, 213L, 244L, 274L, 305L, 335L) # days diff of 1st of month to 1 January + (x$year - self$origin$year) * 366L + yd0[x$month] - yd0[self$origin$month] + x$day - self$origin$day + }, + + #' @description Calculate date parts from day differences from the origin. This + #' only deals with days as these are impacted by the calendar. + #' Hour-minute-second timestamp parts are handled in [CFCalendar]. + #' + #' @param x Integer vector of days to add to the origin. + #' @return A `data.frame` with columns 'year', 'month' and 'day' and as many + #' rows as the length of vector `x`. + offset2date = function(x) { + month <- c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L) + + # First process full years over the vector + yr <- self$origin$year + (x %/% 366L) + x <- x %% 366L + + # Remaining portion relative to the origin + x <- x + self$origin$day + ymd <- mapply(function(y, m, d) { + while (d > month[m]) { + d <- d - month[m] + m <- m + 1L + if (m == 13L) { + y <- y + 1L + m <- 1L + } + } + return(c(y, m, d)) + }, yr, self$origin$month, x) + data.frame(year = ymd[1L,], month = ymd[2L,], day = ymd[3L,], row.names = NULL) + } + ) +) diff --git a/R/CFCalendarJulian.R b/R/CFCalendarJulian.R new file mode 100644 index 0000000..af6b8f0 --- /dev/null +++ b/R/CFCalendarJulian.R @@ -0,0 +1,157 @@ +#' @title Julian CF calendar +#' +#' @description This class represents a Julian calendar of 365 days per year, +#' with every fourth year being a leap year of 366 days. The months and the +#' year align with the standard calendar. This calendar is not compatible with +#' the standard POSIXt calendar. +#' +#' This calendar starts on 1 January of year 1: 0001-01-01 00:00:00. Any dates +#' before this will generate an error. +#' +#' @aliases CFCalendarJulian +#' @docType class +CFCalendarJulian <- R6::R6Class("CFCalendarJulian", + inherit = CFCalendar, + public = list( + #' @description Create a new CF calendar. + #' @param nm The name of the calendar. This must be "julian". This argument + #' is superfluous but maintained to be consistent with the initialization + #' methods of the parent and sibling classes. + #' @param definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + #' @return A new instance of this class. + initialize = function(nm, definition) { + super$initialize(nm, definition) + }, + + #' @description Indicate which of the supplied dates are valid. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return Logical vector with the same length as argument `ymd` has rows + #' with `TRUE` for valid days and `FALSE` for invalid days, or `NA` where + #' the row in argument `ymd` has `NA` values. + valid_days = function(ymd) { + ymd$year >= 1L & ymd$month >= 1L & ymd$month <= 12L & ymd$day >= 1L & + ifelse(self$leap_year(ymd$year), + ymd$day <= c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[ymd$month], + ymd$day <= c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[ymd$month]) + }, + + #' @description Determine the number of days in the month of the calendar. + #' @param ymd `data.frame`, optional, with dates parsed into their parts. + #' @return A vector indicating the number of days in each month for the + #' dates supplied as argument `ymd`. If no dates are supplied, the number + #' of days per month for the calendar as a vector of length 12, for a + #' regular year without a leap day. + month_days = function(ymd = NULL) { + if (is.null(ymd)) return(c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)) + + ifelse(self$leap_year(ymd$year), + c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[ymd$month], + c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[ymd$month]) + }, + + #' @description Indicate which years are leap years. + #' @param yr Integer vector of years to test. + #' @return Logical vector with the same length as argument `yr`. `NA` is + #' returned where elements in argument `yr` are `NA`. + leap_year = function(yr) { + yr %% 4L == 0L + }, + + #' @description Calculate difference in days between a `data.frame` of time + #' parts and the origin. + #' + #' @param x `data.frame`. Dates to calculate the difference for. + #' @return Integer vector of a length equal to the number of rows in + #' argument `x` indicating the number of days between `x` and the origin + #' of the calendar, or `NA` for rows in `x` with `NA` values. + date2offset = function(x) { + .julian_date2offset(x, self$origin) + }, + + #' @description Calculate date parts from day differences from the origin. This + #' only deals with days as these are impacted by the calendar. + #' Hour-minute-second timestamp parts are handled in [CFCalendar]. + #' + #' @param x Integer vector of days to add to the origin. + #' @return A `data.frame` with columns 'year', 'month' and 'day' and as many + #' rows as the length of vector `x`. + offset2date = function(x) { + common_days <- c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L) + leap_days <- c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L) + + # Is the leap day to consider ahead in the year from the base date (offset = 0) or in the next year (offset = 1) + offset <- as.integer(self$origin$month > 2L) + + # First process 4-year cycles of 1,461 days over the vector + yr <- self$origin$year + (x %/% 1461L) * 4L + x <- x %% 1461L + + # Remaining portion relative to the origin + x <- x + self$origin$day + ymd <- mapply(function(y, m, d) { + repeat { + leap <- (y + offset) %% 4L == 0L + ydays <- 365L + as.integer(leap) + + if (d <= 0L) { + d <- d + ydays + y <- y - 1L + if (d > 0L) break + } else if (d > ydays) { + d <- d - ydays + y <- y + 1L + } else break + } + + month <- if (leap) leap_days else common_days + + while (d > month[m]) { + d <- d - month[m] + m <- m + 1L + if (m == 13L) { + y <- y + 1L + m <- 1L + } + } + return(c(y, m, d)) + }, yr, self$origin$month, x) + data.frame(year = ymd[1L,], month = ymd[2L,], day = ymd[3L,], row.names = NULL) + } + ) +) + +# Internal function to calculate dates from offsets. This function is here +# because it is used by CFCalendarStandard. See further description in the +# methods. +.julian_date2offset <- function(x, origin) { + # days diff of 1st of month to 1 January in normal year + yd0 <- c(0L, 31L, 59L, 90L, 120L, 151L, 181L, 212L, 243L, 273L, 304L, 334L) + + origin_year <- origin$year + days_into_year <- yd0[origin$month] + origin$day + if (origin$month <= 2L && origin_year %% 4L == 0L) + days_into_year <- days_into_year - 1L + + mapply(function(y, m, d) { + if (is.na(y)) return(NA_integer_) + + # Adjust for where the leap day falls + if (y >= origin_year) + days <- if (m <= 2L && y %% 4L == 0L) -1L else 0L + else + days <- if (m > 2L && y %% 4L == 0L) 0L else -1L + + repeat { + if (y > origin_year) { + days <- days + 365L + as.integer(y %% 4L == 0L) + y <- y - 1L + } else if (y < origin_year) { + days <- days - 365L - as.integer(y %% 4L == 0L) + y <- y + 1L + } else break + } + days + yd0[m] + d - days_into_year + }, x$year, x$month, x$day) +} diff --git a/R/CFCalendarProleptic.R b/R/CFCalendarProleptic.R new file mode 100644 index 0000000..d5ceebb --- /dev/null +++ b/R/CFCalendarProleptic.R @@ -0,0 +1,96 @@ +#' @title Proleptic Gregorian CF calendar +#' +#' @description This class represents a standard CF calendar, but with the +#' Gregorian calendar extended backwards to before the introduction of the +#' Gregorian calendar. This calendar is compatible with the standard POSIXt +#' calendar, but note that daylight savings time is not considered. +#' +#' This calendar includes dates 1582-10-14 to 1582-10-05 (the gap between the +#' Gregorian and Julian calendars, which is observed by the standard +#' calendar), and extends to years before the year 1, including year 0. +#' +#' @aliases CFCalendarProleptic +#' @docType class +CFCalendarProleptic <- R6::R6Class("CFCalendarProleptic", + inherit = CFCalendar, + public = list( + #' @description Create a new CF calendar. + #' @param nm The name of the calendar. This must be "proleptic_gregorian". + #' This argument is superfluous but maintained to be consistent with the + #' initialization methods of the parent and sibling classes. + #' @param definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + #' @return A new instance of this class. + initialize = function(nm, definition) { + super$initialize(nm, definition) + }, + + #' @description Indicate which of the supplied dates are valid. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return Logical vector with the same length as argument `ymd` has rows + #' with `TRUE` for valid days and `FALSE` for invalid days, or `NA` where + #' the row in argument `ymd` has `NA` values. + valid_days = function(ymd) { + ymd$year & ymd$month >= 1L & ymd$month <= 12L & ymd$day >= 1L & + ifelse(self$leap_year(ymd$year), + ymd$day <= c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[ymd$month], + ymd$day <= c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[ymd$month]) + }, + + #' @description Determine the number of days in the month of the calendar. + #' @param ymd `data.frame`, optional, with dates parsed into their parts. + #' @return Integer vector indicating the number of days in each month for + #' the dates supplied as argument `ymd`. If no dates are supplied, the + #' number of days per month for the calendar as a vector of length 12, for + #' a regular year without a leap day. + month_days = function(ymd = NULL) { + if (is.null(ymd)) return(c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)) + + ifelse(self$leap_year(ymd$year), + c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month], + c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month]) + }, + + #' @description Indicate which years are leap years. + #' @param yr Integer vector of years to test. + #' @return Logical vector with the same length as argument `yr`. `NA` is + #' returned where elements in argument `yr` are `NA`. + leap_year = function(yr) { + ((yr %% 4L == 0L) & (yr %% 100L > 0L)) | (yr %% 400L == 0L) + }, + + #' @description Indicate if the time series described using this calendar + #' can be safely converted to a standard date-time type (`POSIXct`, + #' `POSIXlt`, `Date`). + #' @param offsets The offsets from the CFtime instance. + #' @return `TRUE`. + POSIX_compatible = function(offsets) { + TRUE + }, + + #' @description Calculate difference in days between a `data.frame` of time + #' parts and the origin. + #' + #' @param x `data.frame`. Dates to calculate the difference for. + #' @return Integer vector of a length equal to the number of rows in + #' argument `x` indicating the number of days between `x` and the `origin`, + #' or `NA` for rows in `x` with `NA` values. + date2offset = function(x) { + origin <- lubridate::make_date(self$origin$year, self$origin$month, self$origin$day) + as.integer(lubridate::make_date(x$year, x$month, x$day) - origin) + }, + + #' @description Calculate date parts from day differences from the origin. This + #' only deals with days as these are impacted by the calendar. + #' Hour-minute-second timestamp parts are handled in [CFCalendar]. + #' + #' @param x Integer vector of days to add to the origin. + #' @return A `data.frame` with columns 'year', 'month' and 'day' and as many + #' rows as the length of vector `x`. + offset2date = function(x) { + dt <- lubridate::make_date(self$origin$year, self$origin$month, self$origin$day) + lubridate::days(x) + data.frame(year = lubridate::year(dt), month = lubridate::month(dt), day = lubridate::mday(dt), row.names = NULL) + } + ) +) diff --git a/R/CFCalendarStandard.R b/R/CFCalendarStandard.R new file mode 100644 index 0000000..00ac967 --- /dev/null +++ b/R/CFCalendarStandard.R @@ -0,0 +1,209 @@ +#' @title Standard CF calendar +#' +#' @description This class represents a standard calendar of 365 or 366 days per +#' year. This calendar is compatible with the standard POSIXt calendar for +#' periods after the introduction of the Gregorian calendar, 1582-10-15 +#' 00:00:00. The calendar starts at 0001-01-01 00:00:00, e.g. the start of the +#' Common Era. +#' +#' Note that this calendar, despite its name, is not the same as that used in +#' ISO8601 or many computer systems for periods prior to the introduction of +#' the Gregorian calendar. Use of the "proleptic_gregorian" calendar is +#' recommended for periods before or straddling the introduction date, as that +#' calendar is compatible with POSIXt on most OSes. +#' +#' @importFrom lubridate make_date year month mday days +#' @aliases CFCalendarStandard +#' @docType class +CFCalendarStandard <- R6::R6Class("CFCalendarStandard", + inherit = CFCalendar, + public = list( + #' @field gap The integer offset for 1582-10-15 00:00:00, when the Gregorian + #' calendar started, or 1582-10-05, when the gap between Julian and + #' Gregorian calendars started. The former is set when the calendar origin + #' is more recent, the latter when the origin is prior to the gap. + gap = -1L, + + #' @description Create a new CF calendar. + #' @param nm The name of the calendar. This must be "standard" or + #' "gregorian" (deprecated). + #' @param definition The string that defines the units and the origin, as + #' per the CF Metadata Conventions. + #' @return A new instance of this class. + initialize = function(nm, definition) { + super$initialize(nm, definition) + + self$gap <- if (self$is_gregorian_date(self$origin)) + as.integer(lubridate::make_date(1582, 10, 15) - + lubridate::make_date(self$origin$year, self$origin$month, self$origin$day)) + else + .julian_date2offset(data.frame(year = 1582, month = 10, day = 5), self$origin) + }, + + #' @description Indicate which of the supplied dates are valid. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return Logical vector with the same length as argument `ymd` has rows + #' with `TRUE` for valid days and `FALSE` for invalid days, or `NA` where + #' the row in argument `ymd` has `NA` values. + valid_days = function(ymd) { + ymd$year >= 1L & ymd$month >= 1L & ymd$month <= 12L & ymd$day >= 1L & + ifelse(self$is_gregorian_date(ymd), + # Gregorian calendar + ifelse(self$leap_year(ymd$year), + ymd$day <= c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month], + ymd$day <= c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month]), + # Julian calendar + ifelse(ymd$year == 1582L & ymd$month == 10L & ymd$day > 4L, + FALSE, # days 1582-10-05 - 1582-10-14 do not exist + ifelse(ymd$year %% 4L == 0L, + ymd$day <= c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month], + ymd$day <= c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month]) + ) + ) + }, + + #' @description Indicate which of the supplied dates are in the Gregorian + #' part of the calendar, e.g. 1582-10-15 or after. + #' @param ymd `data.frame` with dates parsed into their parts in columns + #' `year`, `month` and `day`. Any other columns are disregarded. + #' @return Logical vector with the same length as argument `ymd` has rows + #' with `TRUE` for days in the Gregorian part of the calendar and `FALSE` + #' otherwise, or `NA` where the row in argument `ymd` has `NA` values. + is_gregorian_date = function(ymd) { + ymd$year > 1582L | (ymd$year == 1582L & (ymd$month > 10L | (ymd$month == 10L & ymd$day >= 15L))) + }, + + #' @description Indicate if the time series described using this calendar + #' can be safely converted to a standard date-time type (`POSIXct`, + #' `POSIXlt`, `Date`). This is only the case if all offsets are for + #' timestamps fall on or after the start of the Gregorian calendar, + #' 1582-10-15 00:00:00. + #' @param offsets The offsets from the CFtime instance. + #' @return `TRUE`. + POSIX_compatible = function(offsets) { + all(offsets >= self$gap) + }, + + #' @description Determine the number of days in the month of the calendar. + #' @param ymd `data.frame`, optional, with dates parsed into their parts. + #' @return A vector indicating the number of days in each month for the + #' dates supplied as argument `ymd`. If no dates are supplied, the number + #' of days per month for the calendar as a vector of length 12, for a + #' regular year without a leap day. + month_days = function(ymd = NULL) { + if (is.null(ymd)) return(c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)) + + ifelse(self$leap_year(ymd$year), + c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month], + c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[ymd$month]) + }, + + #' @description Indicate which years are leap years. + #' @param yr Integer vector of years to test. + #' @return Logical vector with the same length as argument `yr`. `NA` is + #' returned where elements in argument `yr` are `NA`. + leap_year = function(yr) { + ifelse(yr <= 1582L, + yr %% 4L == 0L, + ((yr %% 4L == 0L) & (yr %% 100L > 0L)) | (yr %% 400L == 0L) + ) + }, + + #' @description Calculate difference in days between a `data.frame` of time + #' parts and the origin. + #' + #' @param x `data.frame`. Dates to calculate the difference for. + #' @return Integer vector of a length equal to the number of rows in + #' argument `x` indicating the number of days between `x` and the origin + #' of the calendar, or `NA` for rows in `x` with `NA` values. + date2offset = function(x) { + if (self$gap > 0L) { + # self$origin in Julian calendar part + greg0 <- lubridate::make_date(1582, 10, 15) + ifelse(self$is_gregorian_date(x), + # Calculate Gregorian dates from 1582-10-15, add gap + as.integer(lubridate::make_date(x$year, x$month, x$day) - greg0) + self$gap, + # Calculate julian days from self$origin + .julian_date2offset(x, self$origin) + ) + } else { + # self$origin in Gregorian calendar part + self_origin <- lubridate::make_date(self$origin$year, self$origin$month, self$origin$day) + julian0 <- data.frame(year = 1582L, month = 10L, day = 5L) + ifelse(self$is_gregorian_date(x), + # Calculate Gregorian dates from self$origin + as.integer(lubridate::make_date(x$year, x$month, x$day) - self_origin), + # Calculate julian days from 1582-10-05, add gap + .julian_date2offset(x, julian0) + self$gap + ) + } + }, + + #' @description Calculate date parts from day differences from the origin. This + #' only deals with days as these are impacted by the calendar. + #' Hour-minute-second timestamp parts are handled in [CFCalendar]. + #' + #' @param x Integer vector of days to add to the origin. + #' @return A `data.frame` with columns 'year', 'month' and 'day' and as many + #' rows as the length of vector `x`. + offset2date = function(x) { + if (self$gap <= 0L && all(x >= self$gap, na.rm = TRUE)) { + # If self$origin and all offsets are in the Gregorian calendar, use + # lubridate. Presumed to cover the majority of cases. + dt <- lubridate::make_date(self$origin$year, self$origin$month, self$origin$day) + lubridate::days(x) + data.frame(year = lubridate::year(dt), month = lubridate::month(dt), day = lubridate::mday(dt), row.names = NULL) + } else { + # Manage cases where self$origin is in the Julian calendar and/or `x` + # values straddle the Julian/Gregorian boundary. + common_days <- c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L) + leap_days <- c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L) + + # Is the leap day to consider ahead in the year from the base date (offset = 0) or in the next year (offset = 1) + offset <- as.integer(self$origin$month > 2L) + + # Correct `x` values that straddle the gap from self$origin + if (self$gap <= 0L) { # Gregorian origin + ndx <- which(x < self$gap) + x[ndx] <- x[ndx] - 10L + } else { # Julian origin + ndx <- which(x >= self$gap) + if (length(ndx)) + x[ndx] <- x[ndx] + 10L + } + + x <- x + self$origin$day + ymd <- mapply(function(y, m, d) { + repeat { + test <- y + offset + leap <- if (test <= 1582) (test) %% 4L == 0L + else ((test %% 4L == 0L) && (test %% 100L > 0L)) || (test %% 400L == 0L) + ydays <- 365L + as.integer(leap) + + if (d <= 0L) { + d <- d + ydays + y <- y - 1L + if (d > 0L) break + } else if (d > ydays) { + d <- d - ydays + y <- y + 1L + } else break + } + + month <- if (leap) leap_days else common_days + + while (d > month[m]) { + d <- d - month[m] + m <- m + 1L + if (m == 13L) { + y <- y + 1L + m <- 1L + } + } + return(c(y, m, d)) + }, self$origin$year, self$origin$month, x) + data.frame(year = ymd[1L,], month = ymd[2L,], day = ymd[3L,], row.names = NULL) + } + } + ) +) diff --git a/R/CFbounds.R b/R/CFbounds.R deleted file mode 100644 index fc148d2..0000000 --- a/R/CFbounds.R +++ /dev/null @@ -1,115 +0,0 @@ -#' Indicates if the time series has equidistant time steps -#' -#' This function returns `TRUE` if the time series has uniformly distributed -#' time steps between the extreme values, `FALSE` otherwise. First test without -#' sorting; this should work for most data sets. If not, only then offsets are -#' sorted. For most data sets that will work but for implied resolutions of -#' month, season, year, etc based on a "days" or finer datum unit this will fail -#' due to the fact that those coarser units have a variable number of days per -#' time step, in all calendars except for `360_day`. For now, an approximate -#' solution is used that should work in all but the most non-conformal exotic -#' arrangements. -#' -#' This function should only be called after offsets have been added. -#' -#' This is an internal function that should not be used outside of the CFtime -#' package. -#' -#' @param x CFtime. The time series to operate on. -#' -#' @returns `TRUE` if all time steps are equidistant, `FALSE` otherwise. -#' -#' @noRd -.ts_equidistant <- function(x) { - out <- all(diff(x@offsets) == x@resolution) - if (!out) { - doff <- diff(sort(x@offsets)) - out <- all(doff == x@resolution) - if (!out) { - # Don't try to make sense of totally non-standard arrangements such as - # datum units "years" or "months" describing sub-daily time steps. - # Also, 360_day calendar should be well-behaved so we don't want to get here. - if (x@datum@unit > 4L || x@datum@cal_id == 3L) return(FALSE) - - # Check if we have monthly or yearly data on a finer-scale datum - # This is all rather approximate but should be fine in most cases - # This accommodates middle-of-the-time-period offsets as per the CF Metadata Conventions - # Please report problems at https://github.com/pvanlaake/CFtime/issues - ddays <- range(doff) * CFt$units$per_day[x@datum@unit] - return((ddays[1] >= 28 && ddays[2] <= 31) || # months - (ddays[1] >= 8 && ddays[2] <= 11) || # dekads - (ddays[1] >= 90 && ddays[2] <= 92) || # seasons, quarters - (ddays[1] >= 365 && ddays[2] <= 366)) # years - } - } - out -} - -#' Set the bounds of a CFtime instance -#' -#' @param cf The CFtime instance -#' @param value The bounds to set. Either an array (2, length(cf)) or a logical -#' -#' @returns Returns `cf` invisibly -#' @noRd -.set_bounds <- function(cf, value) { - if (isFALSE(value)) cf@bounds <- FALSE - else if (isTRUE(value)) cf@bounds <- TRUE - else { - off <- cf@offsets - len <- length(off) - - if (len == 0L) - stop("Cannot set bounds when there are no offsets") - - if (is.matrix(value) && is.numeric(value)) { - if (!all(dim(value) == c(2L, len))) - stop("Replacement value has incorrect dimensions") - } else stop("Replacement value must be a numeric matrix or a single logical value") - - if (!(all(value[2L,] >= off) && all(off >= value[1L,]))) - stop("Values of the replacement value must surround the offset values") - - # Compress array to `TRUE`, if regular - if (len > 1L && identical(value[1L,2L:len], value[2L,1L:(len-1L)]) && - diff(range(diff(value[1L,]))) == 0) value <- TRUE - - cf@bounds <- value - } - invisible(cf) -} - -#' Return bounds -#' -#' @param cf The CFtime instance -#' @param format Optional. A string specifying a format for output -#' -#' @returns An array with dims(2, length(offsets)) with values for the bounds. -#' `NULL` if the bounds have not been set. -#' @noRd -.get_bounds <- function (cf, format) { - len <- length(cf@offsets) - if (len == 0L) return(NULL) - - bnds <- cf@bounds - if (is.logical(bnds)) { - if (!bnds) return(NULL) - - b <- seq(from = cf@offsets[1L] - cf@resolution * 0.5, - by = cf@resolution, - length.out = len + 1L) - if (!missing(format)) { - ts <- .offsets2time(b, cf@datum) - b <- .format_format(ts, tz(cf@datum), format) - } - return(rbind(b[1L:len], b[2L:(len+1L)])) - } - - # bnds is a matrix - if (missing(format)) return(bnds) - - ts <- .offsets2time(as.vector(bnds), cf@datum) - b <- .format_format(ts, tz(cf@datum), format) - dim(b) <- c(2L, len) - b -} diff --git a/R/CFdatum.R b/R/CFdatum.R deleted file mode 100644 index 7182f44..0000000 --- a/R/CFdatum.R +++ /dev/null @@ -1,117 +0,0 @@ -#' CFdatum class -#' -#' This internal class stores the information to represent date and time values using -#' the CF conventions. This class is not supposed to be used by end-users directly. -#' An instance is created by the exported `CFtime` class, which also exposes the -#' relevant properties of this class. -#' -#' The following calendars are supported: -#' -#' \itemize{ -#' \item `gregorian` or `standard`, the international standard calendar for civil use. -#' \item `proleptic_gregorian`, the standard calendar but extending before 1582-10-15 -#' when the Gregorian calendar was adopted. -#' \item `noleap` or `365_day`, all years have 365 days. -#' \item `all_leap` or `366_day`, all years have 366 days. -#' \item `360_day`, all years have 360 days, divided over 12 months of 30 days. -#' \item `julian`, every fourth year is a leap year (so including the years 1700, 1800, 1900, 2100, etc). -#' } -#' -#' @slot definition character. The string that defines the time unit and base date/time. -#' @slot unit numeric. The unit of time in which offsets are expressed. -#' @slot origin data.frame. Data frame with 1 row that defines the origin time. -#' @slot calendar character. The CF-calendar for the instance. -#' @slot cal_id numeric. The internal identifier of the CF-calendar to use. -#' -#' @returns An object of class CFdatum -#' @export -setClass("CFdatum", - slots = c( - definition = "character", - unit = "numeric", - origin = "data.frame", - calendar = "character", - cal_id = "numeric" - )) - -#' Create a CFdatum object -#' -#' This function creates an instance of the `CFdatum` class. After creation the -#' instance is read-only. The parameters to the call are typically read from a -#' CF-compliant data file with climatological observations or predictions. -#' -#' @param definition character. A character string describing the time coordinate -#' of a CF-compliant data file. -#' @param calendar character. A character string describing the calendar to use -#' with the time dimension definition string. -#' -#' @returns An object of the `CFdatum` class. -#' @export -CFdatum <- function(definition, calendar) { - stopifnot(length(definition) == 1L, length(calendar) == 1L) - calendar <- tolower(calendar) - - parts <- strsplit(definition, " ")[[1L]] - if ((length(parts) < 3L) || !(tolower(parts[2L]) %in% c("since", "after", "from", "ref", "per"))) - stop("Definition string does not appear to be a CF-compliant time coordinate description") - u <- which(CFt$CFunits$unit == tolower(parts[1L])) - if (length(u) == 0L) stop("Unsupported unit: ", parts[1L]) - - cal <- CFt$calendars$id[which(calendar == CFt$calendars$name)] - if (length(cal) == 0L) stop("Invalid calendar specification") - - nw <- methods::new("CFdatum", definition = definition, unit = CFt$CFunits$id[u], origin = data.frame(), calendar = calendar, cal_id = cal) - - dt <- .parse_timestamp(nw, paste(parts[3L:length(parts)], collapse = " ")) - if (is.na(dt$year[1L])) - stop("Definition string does not appear to be a CF-compliant time coordinate description: invalid base date specification") - nw@origin <- dt - - return(nw) -} - -setMethod("show", "CFdatum", function(object) { - if (object@origin$tz[1L] == "+0000") tz = "" else tz = object@origin$tz[1L] - cat("CF datum of origin:", - "\n Origin : ", origin_date(object), " ", origin_time(object), tz, - "\n Units : ", CFt$units$name[object@unit], - "\n Calendar: ", object@calendar, "\n", - sep = "") -}) - -#' Equivalence of CFdatum objects -#' -#' This function can be used to test if two `CFdatum` objects represent the same datum -#' for CF-convention time coordinates. Two `CFdatum` objects are considered equivalent -#' if they have the same definition string and the same calendar. Calendars -#' "standard", "gregorian" and "proleptic_gregorian" are considered equivalent, -#' as are the pairs of "365_day" and "no_leap", and "366_day" and "all_leap". -#' -#' @param e1,e2 CFdatum Instances of the CFdatum class. -#' -#' @returns `TRUE` if the `CFdatum` objects are equivalent, `FALSE` otherwise. -#' @noRd -.datum_equivalent <- function(e1, e2) { - sum(e1@origin[1L,1L:6L] != e2@origin[1L,1L:6L]) == 0L && # Offset column is NA - e1@unit == e2@unit && - e1@cal_id == e2@cal_id -} - -#' Compatibility of CFdatum objects -#' -#' This function can be used to test if two `CFdatum` objects have the same unit -#' and calendar for CF-convention time coordinates. Calendars "standard", -#' "gregorian" and "proleptic_gregorian" are considered compatible, as are the -#' pairs of "365_day" and "no_leap", and "366_day" and "all_leap". -#' -#' @param e1,e2 CFdatum Instances of the CFdatum class. -#' -#' @returns `TRUE` if the `CFdatum` objects are compatible, `FALSE` otherwise. -#' @noRd -.datum_compatible <- function(e1, e2) e1@unit == e2@unit && e1@cal_id == e2@cal_id - -origin_date <- function(x) sprintf("%04d-%02d-%02d", x@origin$year[1L], x@origin$month[1L], x@origin$day[1L]) - -origin_time <- function(x) .format_time(x@origin) - -tz <- function(x) x@origin$tz[1L] diff --git a/R/CFfactor.R b/R/CFfactor.R deleted file mode 100644 index 0f2009a..0000000 --- a/R/CFfactor.R +++ /dev/null @@ -1,416 +0,0 @@ -#' Create a factor from the offsets in an CFtime instance -#' -#' With this function a factor can be generated for the time series, or a part -#' thereof, contained in the `CFtime` instance. This is specifically interesting -#' for creating factors from the date part of the time series that aggregate the -#' time series into longer time periods (such as month) that can then be used to -#' process daily CF data sets using, for instance, `tapply()`. -#' -#' The factor will respect the calendar of the datum that the time series is -#' built on. For `period`s longer than a day this will result in a factor where -#' the calendar is no longer relevant (because calendars impacts days, not -#' dekads, months, quarters, seasons or years). -#' -#' The factor will be generated in the order of the offsets of the `CFtime` -#' instance. While typical CF-compliant data sources use ordered time series -#' there is, however, no guarantee that the factor is ordered as multiple -#' `CFtime` objects may have been merged out of order. -#' -#' If the `epoch` parameter is specified, either as a vector of years to include -#' in the factor, or as a list of such vectors, the factor will only consider -#' those values in the time series that fall within the list of years, inclusive -#' of boundary values. Other values in the factor will be set to `NA`. The years -#' need not be contiguous, within a single vector or among the list items, or in -#' order. -#' -#' The following periods are supported by this function: -#' -#' \itemize{ -#' \item `year`, the year of each offset is returned as "YYYY". -#' \item `season`, the meteorological season of each offset is returned as -#' "Sx", with x being 1-4, preceeded by "YYYY" if no `epoch` is -#' specified. Note that December dates are labeled as belonging to the -#' subsequent year, so the date "2020-12-01" yields "2021S1". This implies -#' that for standard CMIP files having one or more full years of data the -#' first season will have data for the first two months (January and -#' February), while the final season will have only a single month of data -#' (December). -#' \item `quarter`, the calendar quarter of each offset is returned as "Qx", -#' with x being 1-4, preceeded by "YYYY" if no `epoch` is specified. -#' \item `month`, the month of each offset is returned as "01" to -#' "12", preceeded by "YYYY-" if no `epoch` is specified. This is the default -#' period. -#' \item `dekad`, ten-day periods are returned as -#' "Dxx", where xx runs from "01" to "36", preceeded by "YYYY" if no `epoch` -#' is specified. Each month is subdivided in dekads as follows: 1- days 01 - -#' 10; 2- days 11 - 20; 3- remainder of the month. -#' \item `day`, the month and day of each offset are returned as "MM-DD", -#' preceeded by "YYYY-" if no `epoch` is specified. -#' } -#' -#' It is not possible to create a factor for a period that is shorter than the -#' temporal resolution of the source data set from which the `cf` argument -#' derives. As an example, if the source data set has monthly data, a dekad or -#' day factor cannot be created. -#' -#' Creating factors for other periods is not supported by this function. Factors -#' based on the timestamp information and not dependent on the calendar can -#' trivially be constructed from the output of the [as_timestamp()] function. -#' -#' For non-epoch factors the attribute 'CFtime' of the result contains a CFtime -#' instance that is valid for the result of applying the factor to a data set -#' that the `cf` argument is associated with. In other words, if CFtime instance -#' 'Acf' describes the temporal dimension of data set 'A' and a factor 'Af' is -#' generated from 'Acf', then `attr(Af, "CFtime")` describes the temporal -#' dimension of the result of, say, `apply(A, 1:2, tapply, Af, FUN)`. The -#' 'CFtime' attribute is `NULL` for epoch factors. -#' -#' @param cf CFtime. An instance of the `CFtime` class whose offsets will -#' be used to construct the factor. -#' @param period character. A character string with one of the values -#' "year", "season", "quarter", "month" (the default), "dekad" or "day". -#' @param epoch numeric or list, optional. Vector of years for which to -#' construct the factor, or a list whose elements are each a vector of years. -#' If `epoch` is not specified, the factor will use the entire time series for -#' the factor. -#' -#' @returns If `epoch` is a single vector or not specified, a factor with a -#' length equal to the number of offsets in `cf`. If `epoch` is a list, a list -#' with the same number of elements and names as `epoch`, each containing a -#' factor. Elements in the factor will be set to `NA` for time series values -#' outside of the range of specified years. -#' -#' The factor, or factors in the list, have attributes 'period', 'epoch' and -#' 'CFtime'. Attribute 'period' holds the value of the `period` argument. -#' Attribute 'epoch' indicates the number of years that are included in the -#' epoch, or -1 if no `epoch` is provided. Attribute 'CFtime' holds an -#' instance of CFtime that has the same definition as `cf`, but with offsets -#' corresponding to the mid-point of non-epoch factor levels; if the `epoch` -#' argument is specified, attribute 'CFtime' is `NULL`. -#' @seealso [cut()] creates a non-epoch factor for arbitrary cut points. -#' @export -#' -#' @examples -#' cf <- CFtime("days since 1949-12-01", "360_day", 19830:54029) -#' -#' # Create a dekad factor for the whole time series -#' f <- CFfactor(cf, "dekad") -#' -#' # Create three monthly factors for early, mid and late 21st century epochs -#' ep <- CFfactor(cf, epoch = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) -CFfactor <- function(cf, period = "month", epoch = NULL) { - if (!(methods::is(cf, "CFtime"))) stop("First argument to CFfactor() must be an instance of the `CFtime` class") - if (length(cf@offsets) < 10L) stop("Cannot create a factor for very short time series") - - period <- tolower(period) - if (!((length(period) == 1L) && (period %in% CFt$factor_periods))) - stop("Period specifier must be a single value of a supported period") - - # No fine-grained period factors for coarse source data - timestep <- CFt$units$seconds[cf@datum@unit] * cf@resolution; - if ((period == "year") && (timestep > 86400 * 366) || - (period %in% c("season", "quarter")) && (timestep > 86400 * 90) || # Somewhat arbitrary - (period == "month") && (timestep > 86400 * 31) || - (period == "dekad") && (timestep > 86400) || # Must be constructed from daily or finer data - (period == "day") && (timestep > 86400)) # Must be no longer than a day - stop("Cannot produce a short period factor from source data with long time interval") - - time <- .offsets2time(cf@offsets, cf@datum) - months <- c("01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12") - - if (is.null(epoch)) { - # Create the factor for the specified period as well as bounds dates for a - # new CFtime instance for the factor. Lower bounds for the factor level is - # easy, upper bound of last level takes effort. - switch(period, - "year" = { - out <- as.factor(sprintf("%04d", time$year)) - l <- levels(out) - dt <- c(paste0(l, "-01-01"), sprintf("%04d-01-01", as.integer(l[nlevels(out)]) + 1L)) - }, - "season" = { - if (!requireNamespace("stringr")) - stop("Must install package `stringr` to use this functionality.") - - out <- as.factor( - ifelse(time$month == 12L, sprintf("%04dS1", time$year + 1L), - sprintf("%04dS%d", time$year, time$month %/% 3L + 1L))) - l <- levels(out) - dt <- ifelse(substr(l, 6L, 6L) == "1", paste0(as.integer(substr(l, 1L, 4L)) - 1L, "-12-01"), - stringr::str_replace_all(l, c("S2" = "-03-01", "S3" = "-06-01", "S4" = "-09-01"))) - ll <- l[nlevels(out)] - lp <- as.integer(substr(ll, 6L, 6L)) - if (lp == 1L) - dt <- c(dt, sprintf("%04d-03-01", as.integer(substr(ll, 1L, 4L)) + 1L)) - else dt <- c(dt, sprintf("%s-%02d-01", substr(ll, 1L, 4L), lp * 3L)) - }, - "quarter" = { - if (!requireNamespace("stringr")) - stop("Must install package `stringr` to use this functionality.") - - out <- as.factor(sprintf("%04dQ%d", time$year, (time$month - 1L) %/% 3L + 1L)) - l <- levels(out) - dt <- stringr::str_replace_all(l, c("Q1" = "-01-01", "Q2" = "-04-01", "Q3" = "-07-01", "Q4" = "-10-01")) - ll <- l[nlevels(out)] - lp <- as.integer(substr(ll, 6L, 6L)) - if (lp == 4L) - dt <- c(dt, sprintf("%04d-01-01", as.integer(substr(ll, 1L, 4L)) + 1L)) - else dt <- c(dt, sprintf("%s-%02d-01", substr(ll, 1L, 4L), lp * 3L + 1L)) - }, - "month" = { - out <- as.factor(sprintf("%04d-%s", time$year, months[time$month])) - l <- levels(out) - dt <- paste0(l, "-01") - ll <- l[nlevels(out)] - lp <- as.integer(substr(ll, 6L, 7L)) - if (lp == 12L) - dt <- c(dt, sprintf("%04d-01-01", as.integer(substr(ll, 1L, 4L)) + 1L)) - else dt <- c(dt, sprintf("%s-%02d-01", substr(ll, 1L, 4L), lp + 1L)) - }, - "dekad" = { - out <- as.factor(sprintf("%04dD%02d", time$year, (time$month - 1L) * 3L + pmin.int((time$day - 1L) %/% 10L + 1L, 3L))) - l <- levels(out) - dk <- as.integer(substr(l, 6L, 7L)) - 1L - dt <- sprintf("%s-%02d-%s", substr(l, 1L, 4L), dk %/% 3L + 1L, c("01", "11", "21")[dk %% 3L + 1L]) - ll <- l[nlevels(out)] - lp <- as.integer(substr(ll, 6L, 7L)) - yr <- as.integer(substr(lp, 1L, 4L)) - if (lp == 36L) - dt <- c(dt, sprintf("%04d-01-01", yr + 1L)) - else dt <- c(dt, sprintf("%04d-%02d-%s", yr, (lp + 1L) %/% 3L + 1L, c("01", "11", "21")[(lp + 1L) %% 3L + 1L])) - }, - "day" = { - out <- as.factor(sprintf("%04d-%02d-%02d", time$year, time$month, time$day)) - l <- levels(out) - lp <- l[nlevels(out)] - last <- .offsets2time(.parse_timestamp(cf@datum, lp)$offset, cf@datum) - dt <- c(l, sprintf("%04d-%02d-%02d", last$year, last$month, last$day)) - } - ) - - # Convert bounds dates to an array of offsets, find mid-points, create new CFtime instance - off <- .parse_timestamp(cf@datum, dt)$offset - off[is.na(off)] <- 0 # This can happen only when the time series starts at or close to the datum origin, for seasons - noff <- length(off) - bnds <- rbind(off[1L:(noff - 1L)], off[2L:noff]) - off <- bnds[1L,] + (bnds[2L,] - bnds[1L,]) * 0.5 - new_cf <- CFtime(cf@datum@definition, cf@datum@calendar, off) - bounds(new_cf) <- TRUE - - # Bind attributes to the factor - attr(out, "epoch") <- -1L - attr(out, "period") <- period - attr(out, "CFtime") <- new_cf - return(out) - } - - # Epoch factor - if (is.numeric(epoch)) ep <- list(epoch) - else if ((is.list(epoch) && all(unlist(lapply(epoch, is.numeric))))) ep <- epoch - else stop("When specified, the `epoch` parameter must be a numeric vector or a list thereof") - - out <- lapply(ep, function(years) { - f <- switch(period, - "year" = ifelse(time$year %in% years, sprintf("%04d", time$year), NA_character_), - "season" = ifelse((time$month == 12L) & ((time$year + 1L) %in% years), "S1", - ifelse((time$month < 12L) & (time$year %in% years), sprintf("S%d", time$month %/% 3L + 1L), NA_character_)), - "quarter" = ifelse(time$year %in% years, sprintf("Q%d", (time$month - 1L) %/% 3L + 1L), NA_character_), - "month" = ifelse(time$year %in% years, months[time$month], NA_character_), - "dekad" = ifelse(time$year %in% years, sprintf("D%02d", (time$month - 1L) * 3L + pmin.int((time$day - 1L) %/% 10L + 1L, 3L)), NA_character_), - "day" = ifelse(time$year %in% years, sprintf("%s-%02d", months[time$month], time$day), NA_character_) - ) - f <- as.factor(f) - attr(f, "epoch") <- length(years) - attr(f, "period") <- period - attr(f, "CFtime") <- NULL - f - }) - if (is.numeric(epoch)) out <- out[[1L]] - else names(out) <- names(epoch) - return(out) -} - -#' Number of base time units in each factor level -#' -#' Given a factor as returned by [CFfactor()] and the `CFtime` instance from -#' which the factor was derived, this function will return a numeric vector with -#' the number of time units in each level of the factor. -#' -#' The result of this function is useful to convert between absolute and -#' relative values. Climate change anomalies, for instance, are usually computed -#' by differencing average values between a future period and a baseline period. -#' Going from average values back to absolute values for an aggregate period -#' (which is typical for temperature and precipitation, among other variables) -#' is easily done with the result of this function, without having to consider -#' the specifics of the calendar of the data set. -#' -#' If the factor `f` is for an epoch (e.g. spanning multiple years and the -#' levels do not indicate the specific year), then the result will indicate the -#' number of time units of the period in a regular single year. In other words, -#' for an epoch of 2041-2060 and a monthly factor on a standard calendar with a -#' `days` unit, the result will be `c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)`. -#' Leap days are thus only considered for the `366_day` and `all_leap` calendars. -#' -#' Note that this function gives the number of time units in each level of the -#' factor - the actual number of data points in the `cf` instance per factor -#' level may be different. Use [CFfactor_coverage()] to determine the actual -#' number of data points or the coverage of data points relative to the factor -#' level. -#' -#' @param cf CFtime. An instance of CFtime. -#' @param f factor or list. A factor or a list of factors derived from the -#' parameter `cf`. The factor or list thereof should generally be generated by -#' the function [CFfactor()]. -#' -#' @returns If `f` is a factor, a numeric vector with a length equal to the -#' number of levels in the factor, indicating the number of time units in each -#' level of the factor. If `f` is a list of factors, a list with each element -#' a numeric vector as above. -#' @export -#' -#' @examples -#' cf <- CFtime("days since 2001-01-01", "365_day", 0:364) -#' f <- CFfactor(cf, "dekad") -#' CFfactor_units(cf, f) -CFfactor_units <- function(cf, f) { - if (!(methods::is(cf, "CFtime"))) stop("First argument to `CFfactor_units()` must be an instance of the `CFtime` class") - - if (is.list(f)) factors <- f else factors <- list(f) - if (!(all(unlist(lapply(factors, function(x) is.factor(x) && is.numeric(attr(x, "epoch")) && - attr(x, "period") %in% CFt$factor_periods))))) - stop("Argument `f` must be a factor generated by the function `CFfactor()`") - - cal <- cf@datum@cal_id - upd <- CFt$units$per_day[cf@datum@unit] - out <- lapply(factors, function(fac) .factor_units(fac, cal, upd)) - if (is.factor(f)) out <- out[[1L]] - return(out) -} - -#' Calculate time units in factors -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param f factor. Factor as generated by `CFfactor()`. -#' @param cal numeric. Calendar id of the `CFtime()` instance. -#' @param upd numeric. Number of units per day, from the `CFt` environment. -#' -#' @returns A vector as long as the number of levels in the factor. -#' @noRd -.factor_units <- function(f, cal, upd) { - period <- attr(f, "period") - if (cal == 3L) { - res <- rep(c(360L, 90L, 90L, 30L, 10L, 1L)[which(CFt$factor_periods == period)], nlevels(f)) - } else { - if (attr(f, "epoch") > 0L) { - if (cal %in% c(1L, 2L, 4L)) { - res <- switch(period, - "year" = rep(365L, nlevels(f)), - "season" = c(90L, 92L, 92L, 91L)[as.integer(substr(levels(f), 2, 2))], - "quarter" = c(90L, 91L, 92L, 92L)[as.integer(substr(levels(f), 2, 2))], - "month" = c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[as.integer(levels(f))], - "dekad" = { - dk <- as.integer(substr(levels(f), 2L, 3L)) - ifelse(dk %% 3L > 0L | dk %in% c(12L, 18L, 27L, 33L), 10L, - ifelse(dk %in% c(3L, 9L, 15L, 21L, 24L, 30L, 36L), 11L, 8L)) - }, - "day" = rep(1L, nlevels(f)) - ) - } else if (cal == 5L) { - res <- switch(period, - "year" = rep(366L, nlevels(f)), - "season" = c(91L, 92L, 92L, 91L)[as.integer(substr(levels(f), 2, 2))], - "quarter" = c(91L, 91L, 92L, 92L)[as.integer(levels(f))], - "month" = c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[as.integer(levels(f))], - "dekad" = { - dk <- as.integer(substr(levels(f), 2L, 3L)) - ifelse(dk %% 3L > 0L | dk %in% c(12L, 18L, 27L, 33L), 10L, - ifelse(dk %in% c(3L, 9L, 15L, 21L, 24L, 30L, 36L), 11L, 9L)) - }, - "day" = rep(1L, nlevels(f)) - ) - } - } else { # not an epoch factor - res <- switch(period, - "year" = ifelse(.is_leap_year(as.integer(levels(f)), cal), 366L, 365L), - "season" = { - year <- as.integer(substr(levels(f), 1L, 4L)) - season <- as.integer(substr(levels(f), 6L, 6L)) - ifelse(.is_leap_year(year, cal), c(91L, 92L, 92L, 91L)[season], c(90L, 92L, 92L, 91L)[season]) - }, - "quarter" = { - year <- as.integer(substr(levels(f), 1L, 4L)) - qtr <- as.integer(substr(levels(f), 6L, 6L)) - ifelse(.is_leap_year(year, cal), c(91L, 91L, 92L, 92L)[qtr], c(90L, 91L, 92L, 92L)[qtr]) - }, - "month" = { - year <- as.integer(substr(levels(f), 1L, 4L)) - month <- as.integer(substr(levels(f), 6L, 7L)) - ifelse(.is_leap_year(year, cal), c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[month], - c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[month]) - }, - "dekad" = { - year <- as.integer(substr(levels(f), 1L, 4L)) - dk <- as.integer(substr(levels(f), 6L, 7L)) - ifelse(dk %% 3L > 0L | dk %in% c(12L, 18L, 27L, 33L), 10L, - ifelse(dk %in% c(3L, 9L, 15L, 21L, 24L, 30L, 36L), 11L, - ifelse(.is_leap_year(year, cal), 9L, 8L))) - }, - "day" = rep(1L, nlevels(f)) - ) - } - } - return(res * upd) -} - -#' Coverage of time elements for each factor level -#' -#' This function calculates the number of time elements, or the relative -#' coverage, in each level of a factor generated by [CFfactor()]. -#' -#' @param cf CFtime. An instance of CFtime. -#' @param f factor or list. A factor or a list of factors derived from the -#' parameter `cf`. The factor or list thereof should generally be generated by -#' the function [CFfactor()]. -#' @param coverage "absolute" or "relative". -#' -#' @returns If `f` is a factor, a numeric vector with a length equal to the -#' number of levels in the factor, indicating the number of units from the -#' time series in `cf` contained in each level of the factor when -#' `coverage = "absolute"` or the proportion of units present relative to the -#' maximum number when `coverage = "relative"`. If `f` is a list of factors, a -#' list with each element a numeric vector as above. -#' @export -#' -#' @examples -#' cf <- CFtime("days since 2001-01-01", "365_day", 0:364) -#' f <- CFfactor(cf, "dekad") -#' CFfactor_coverage(cf, f, "absolute") -CFfactor_coverage <- function(cf, f, coverage = "absolute") { - if (!(methods::is(cf, "CFtime"))) stop("First argument to `CFfactor_coverage()` must be an instance of the `CFtime` class") - - if (is.list(f)) factors <- f else factors <- list(f) - if (!(all(unlist(lapply(factors, function(x) is.factor(x) && is.numeric(attr(x, "epoch")) && - attr(x, "period") %in% CFt$factor_periods))))) - stop("Argument `f` must be a factor generated by the function `CFfactor()`") - - if (!(is.character(coverage) && coverage %in% c("absolute", "relative"))) - stop("Argument `coverage` must be a chaarcter string with a value of \"absolute\" or \"relative\"") - - if (coverage == "relative") { - cal <- cf@datum@cal_id - upd <- CFt$units$per_day[cf@datum@unit] - out <- lapply(factors, function(fac) { - res <- tabulate(fac) / .factor_units(fac, cal, upd) - yrs <- attr(fac, "epoch") - if (yrs > 0) res <- res / yrs - return(res) - }) - } else { - out <- lapply(factors, tabulate) - } - - if (is.factor(f)) out <- out[[1L]] - return(out) -} diff --git a/R/CFformat.R b/R/CFformat.R deleted file mode 100644 index bd29e1d..0000000 --- a/R/CFformat.R +++ /dev/null @@ -1,140 +0,0 @@ -#' Create a vector that represents CF timestamps -#' -#' This function generates a vector of character strings or `POSIXct`s that -#' represent the date and time in a selectable combination for each offset. -#' -#' The character strings use the format `YYYY-MM-DDThh:mm:ss±hhmm`, depending on -#' the `format` specifier. The date in the string is not necessarily compatible -#' with `POSIXt` - in the `360_day` calendar `2017-02-30` is valid and -#' `2017-03-31` is not. -#' -#' For the "standard", "gregorian" and "proleptic_gregorian" calendars the -#' output can also be generated as a vector of `POSIXct` values by specifying -#' `asPOSIX = TRUE`. -#' -#' @param cf CFtime. The `CFtime` instance that contains the offsets to use. -#' @param format character. A character string with either of the values "date" or -#' "timestamp". If the argument is not specified, the format used is -#' "timestamp" if there is time information, "date" otherwise. -#' @param asPOSIX logical. If `TRUE`, for "standard", "gregorian" and -#' "proleptic_gregorian" calendars the output is a vector of `POSIXct` - for -#' other calendars the result is `NULL`. Default value is `FALSE`. -#' -#' @seealso The [CFtime::format()] function gives greater flexibility through -#' the use of strptime-like format specifiers. -#' @returns A character vector where each element represents a moment in time -#' according to the `format` specifier. -#' @export -#' -#' @examples -#' cf <- CFtime("hours since 2020-01-01", "standard", seq(0, 24, by = 0.25)) -#' as_timestamp(cf, "timestamp") -#' -#' cf2 <- CFtime("days since 2002-01-21", "standard", 0:20) -#' tail(as_timestamp(cf2, asPOSIX = TRUE)) -#' -#' tail(as_timestamp(cf2)) -#' -#' tail(as_timestamp(cf2 + 1.5)) -as_timestamp <- function(cf, format = NULL, asPOSIX = FALSE) { - if (!(methods::is(cf, "CFtime"))) - stop("First argument to `as_timestamp()` must be an instance of the `CFtime` class") - if (asPOSIX && cf@datum@cal_id != 1L) - stop("Cannot make a POSIX timestamp on a non-standard calendar") - - time <- .offsets2time(cf@offsets, cf@datum) - if (nrow(time) == 0L) return() - - if (is.null(format)) - format <- ifelse(cf@datum@unit < 4L || .has_time(time), "timestamp", "date") - else if (!(format %in% c("date", "timestamp"))) - stop("Format specifier not recognized") - - if (asPOSIX) { - if (format == "date") ISOdate(time$year, time$month, time$day, 0L) - else ISOdatetime(time$year, time$month, time$day, time$hour, time$minute, time$second, "UTC") - } else .format_format(time, timezone(cf), format) -} - -#' Formatting of time strings from time elements -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param t data.frame. A data.frame representing timestamps. -#' -#' @returns A vector of character strings with a properly formatted time. If any -#' timestamp has a fractional second part, then all time strings will report -#' seconds at milli-second precision. -#' @noRd -.format_time <- function(t) { - fsec <- t$second %% 1L - if (any(fsec > 0L)) { - paste0(sprintf("%02d:%02d:", t$hour, t$minute), ifelse(t$second < 10, "0", ""), sprintf("%.3f", t$second)) - } else { - sprintf("%02d:%02d:%02d", t$hour, t$minute, t$second) - } -} - -#' Do the time elements have time-of-day information? -#' -#' If any time information > 0, then `TRUE` otherwise `FALSE` -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param t data.frame. A data.frame representing timestamps. -#' -#' @returns `TRUE` if any timestamp has time-of-day information, `FALSE` otherwise. -#' @noRd -.has_time <- function(t) { - any(t$hour > 0) || any(t$minute > 0) || any(t$second > 0) -} - -#' Do formatting of timestamps with format specifiers -#' -#' Internal function -#' -#' @param ts data.frame of decomposed offsets. -#' @param tz character. Time zone character string. -#' @param format character. A character string with the format specifiers, or -#' "date" or "timestamp". -#' @returns Character vector of formatted timestamps. -#' @noRd -.format_format <- function(ts, tz, format) { - if (format == "") format <- "timestamp" - if (format == "timestamp" && sum(ts$hour, ts$minute, ts$second) == 0) - format <- "date" - - if (format == "date") return(sprintf("%04d-%02d-%02d", ts$year, ts$month, ts$day)) - else if (format == "timestamp") return(sprintf("%04d-%02d-%02d %s", ts$year, ts$month, ts$day, .format_time(ts))) - - # Expand any composite specifiers - format <- stringr::str_replace_all(format, c("%F" = "%Y-%m-%d", "%R" = "%H:%M", "%T" = "%H:%M:%S")) - - # Splice in timestamp values for specifiers - # nocov start - if (grepl("%b|%h", format[1])) { - mon <- strftime(ISOdatetime(2024, 1:12, 1, 0, 0, 0), "%b") - format <- stringr::str_replace_all(format, "%b|%h", mon[ts$month]) - } - if (grepl("%B", format[1])) { - mon <- strftime(ISOdatetime(2024, 1:12, 1, 0, 0, 0), "%B") - format <- stringr::str_replace_all(format, "%B", mon[ts$month]) - } - # nocov end - format <- stringr::str_replace_all(format, "%[O]?d", sprintf("%02d", ts$day)) - format <- stringr::str_replace_all(format, "%e", sprintf("%2d", ts$day)) - format <- stringr::str_replace_all(format, "%[O]?H", sprintf("%02d", ts$hour)) - format <- stringr::str_replace_all(format, "%[O]?I", sprintf("%02d", ts$hour %% 12)) - # "%j" = ??? - format <- stringr::str_replace_all(format, "%[O]?m", sprintf("%02d", ts$month)) - format <- stringr::str_replace_all(format, "%[O]?M", sprintf("%02d", ts$minute)) - format <- stringr::str_replace_all(format, "%p", ifelse(ts$hour < 12, "AM", "PM")) - format <- stringr::str_replace_all(format, "%S", sprintf("%02d", as.integer(ts$second))) - format <- stringr::str_replace_all(format, "%[E]?Y", sprintf("%04d", ts$year)) - format <- stringr::str_replace_all(format, "%z", tz) - format <- stringr::str_replace_all(format, "%%", "%") - format -} - diff --git a/R/CFparse.R b/R/CFparse.R deleted file mode 100644 index 46fbcae..0000000 --- a/R/CFparse.R +++ /dev/null @@ -1,345 +0,0 @@ -#' Parse series of timestamps in CF format to date-time elements -#' -#' This function will parse a vector of timestamps in ISO8601 or UDUNITS format -#' into a data frame with columns for the elements of the timestamp: year, -#' month, day, hour, minute, second, time zone. Those timestamps that could not -#' be parsed or which represent an invalid date in the indicated `CFtime` -#' instance will have `NA` values for the elements of the offending timestamp -#' (which will generate a warning). -#' -#' The supported formats are the *broken timestamp* format from the UDUNITS -#' library and ISO8601 *extended*, both with minor changes, as suggested by the -#' CF Metadata Conventions. In general, the format is `YYYY-MM-DD hh:mm:ss.sss -#' hh:mm`. The year can be from 1 to 4 digits and is interpreted literally, so -#' `79-10-24` is the day Mount Vesuvius erupted and destroyed Pompeii, not -#' `1979-10-24`. The year and month are mandatory, all other fields are -#' optional. There are defaults for all missing values, following the UDUNITS -#' and CF Metadata Conventions. Leading zeros can be omitted in the UDUNITS -#' format, but not in the ISO8601 format. The optional fractional part can have -#' as many digits as the precision calls for and will be applied to the smallest -#' specified time unit. In the result of this function, if the fraction is -#' associated with the minute or the hour, it is converted into a regular -#' `hh:mm:ss.sss` format, i.e. any fraction in the result is always associated -#' with the second, rounded down to milli-second accuracy. The separator between -#' the date and the time can be a single whitespace character or a `T`. -#' -#' The time zone is optional and should have at least the hour or `Z` if -#' present, the minute is optional. The time zone hour can have an optional -#' sign. In the UDUNITS format the separator between the time and the time zone -#' must be a single whitespace character, in ISO8601 there is no separation -#' between the time and the timezone. Time zone names are not supported (as -#' neither UDUNITS nor ISO8601 support them) and will cause parsing to fail when -#' supplied, with one exception: the designator "UTC" is silently dropped (i.e. -#' interpreted as "00:00"). -#' -#' Currently only the extended formats (with separators between the elements) -#' are supported. The vector of timestamps may have any combination of ISO8601 -#' and UDUNITS formats. -#' -#' Timestamps that are prior to the datum are not allowed. The corresponding row -#' in the result will have `NA` values. -#' -#' @param cf CFtime. An instance of `CFtime` indicating the CF calendar and -#' datum to use when parsing the date. -#' @param x character. Vector of character strings representing timestamps in -#' ISO8601 extended or UDUNITS broken format. -#' -#' @returns A data frame with constituent elements of the parsed timestamps in -#' numeric format. The columns are year, month, day, hour, minute, second -#' (with an optional fraction), time zone (character string), and the -#' corresponding offset value from the datum. Invalid input data will appear -#' as `NA` - if this is the case, a warning message will be displayed - other -#' missing information on input will use default values. -#' @export -#' @examples -#' cf <- CFtime("days since 0001-01-01", "proleptic_gregorian") -#' -#' # This will have `NA`s on output and generate a warning -#' timestamps <- c("2012-01-01T12:21:34Z", "12-1-23", "today", -#' "2022-08-16T11:07:34.45-10", "2022-08-16 10.5+04") -#' CFparse(cf, timestamps) -CFparse <- function(cf, x) { - stopifnot(is.character(x), methods::is(cf, "CFtime")) - if (cf@datum@unit > 4) stop("Parsing of timestamps on a \"month\" or \"year\" datum is not supported.") - - out <- .parse_timestamp(cf@datum, x) - if (anyNA(out$year)) - warning("Some dates could not be parsed. Result contains `NA` values.") - if (length(unique(out$tz)) > 1) - warning("Timestamps have multiple time zones. Some or all may be different from the datum time zone.") - else if (out$tz[1] != timezone(cf)) - warning("Timestamps have time zone that is different from the datum.") - return(out) -} - -#' Parsing a vector of date-time strings, using a CFtime specification -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param datum CFdatum. The `CFdatum` instance that is the datum for the dates. -#' @param d character. A vector of strings of dates and times. -#' -#' @returns A data frame with columns year, month, day, hour, minute, second, -#' time zone, and offset. Invalid input data will appear as `NA`. -#' @noRd -.parse_timestamp <- function(datum, d) { - # Parsers - - # UDUNITS broken timestamp definition, with some changes - # broken_timestamp {broken_date}({space|T}+{broken_clock})? -- T not in definition but present in lexer code - # broken_date {year}-{month}(-{day})? - # year [+-]?[0-9]{1,4} - # month 0?[1-9]|1[0-2] - # day 0?[1-9]|[1-2][0-9]|30|31 - # broken_clock {hour}:{minute}(:{second})? - # hour [0-1]?[0-9]|2[0-3] -- sign on hour not allowed, but see timezone - # minute [0-5]?[0-9] - # second {minute}? -- leap second not supported - # fractional part (\.[0-9]*)? - # timezone [+-]?{hour}(:{minute})? -- added, present in lexer code - broken <- paste0( - "^", # anchor string at start - "([+-]?[0-9]{1,4})", # year, with optional sign - "-(0?[1-9]|1[012])", # month - "(?:-(0?[1-9]|[12][0-9]|3[01]))?", # day, optional - "(?:[T ]", # if a time is following, separate with a single whitespace character or a "T" - "([01]?[0-9]|2[0-3])", # hour - ":([0-5]?[0-9])", # minute - "(?::([0-5]?[0-9]))?", # second, optional - "(?:\\.([0-9]*))?", # optional fractional part of the smallest specified unit - ")?", # close optional time capture group - "(?:\\s", # if a time zone offset is following, separate with a single whitespace character - "([+-])?([01]?[0-9]|2[0-3])", # tz hour, with optional sign - "(?::(00|15|30|45))?", # optional tz minute, only 4 possible values - ")?", # close optional timezone capture group - "$" # anchor string at end - ) - - iso8601 <- paste0( - "^", - "([0-9]{4})", - "-(0[1-9]|1[012])", - "-(0[1-9]|[12][0-9]|3[01])?", - "(?:", - "[T ]([01][0-9]|2[0-3])", - "(?::([0-5][0-9]))?", - "(?::([0-5][0-9]))?", - "(?:\\.([0-9]*))?", - ")?", - "(?:([Z+-])([01][0-9]|2[0-3])?(?::(00|15|30|45))?", ## FIXME: Z?, smaller number of captures - ")?$" - ) - - # UDUNITS packed timestamp definition - NOT YET USED - # packed_timestamp {packed_date}({space|T}+{packed_clock})? -- T and space only allowed in packed time follows - # packed_date {year}({month}{day}?)? -- must be YYYYMMDD or else format is ambiguous, as per lexer code - # packed_clock {hour}({minute}{second}?)? -- must be HHMMSS to be unambiguous - # timezone [+-]?{hour}({minute})? -- added, present in lexer code, must be HHMM - # packed <- stringi::stri_join( - # "^", # anchor string at start - # "([+-]?[0-9]{4})", # year, with optional sign - # "(0[1-9]|1[012])?", # month, optional - # "(0[1-9]|[12][0-9]|3[01])?", # day, optional - # "(?:[T,\\s]", # if a time is following, separate with a single whitespace character or a "T" - # "([01][0-9]|2[0-3])?", # hour - # "([0-5][0-9])?", # minute, optional - # "([0-5]?[0-9](?:\\.[0-9]*)?)?", # second, optional, with optional fractional part - # ")?", # close optional time capture group - # "(?:\\s", # if a time zone offset is following, separate with a single whitespace character - # "([+-]?[01][0-9]|2[0-3])?", # hour, with optional sign - # "(00|15|30|45)?", # minute, only 4 possible values - # ")?", # close optional timezone capture group - # "$" # anchor string at end - # ) - - parse <- data.frame(year = integer(), month = integer(), day = integer(), - hour = integer(), minute = integer(), second = numeric(), frac = character(), - tz_sign = character(), tz_hour = character(), tz_min = character()) - - # Drop "UTC", if given - d <- trimws(gsub("UTC$", "", d)) - - cap <- utils::strcapture(iso8601, d, parse) - missing <- which(is.na(cap$year)) - if (length(missing) > 0) - cap[missing,] <- utils::strcapture(broken, d[missing], parse) - - # Assign any fraction to the appropriate time part - cap$frac[is.na(cap$frac)] <- "0" - frac <- as.numeric(paste0("0.", cap$frac)) - if (sum(frac) > 0) { - ndx <- which(!(is.na(cap$second)) & frac > 0) - if (length(ndx) > 0) cap$second[ndx] <- cap$second[ndx] + frac[ndx] - ndx <- which(!(is.na(cap$minute)) & is.na(cap$second) & frac > 0) - if (length(ndx) > 0) cap$second[ndx] <- 60 * frac[ndx] - ndx <- which(!(is.na(cap$hour)) & is.na(cap$minute) & frac > 0) - if (length(ndx) > 0) { - secs <- 3600 * frac - cap$minute[ndx] <- secs[ndx] %/% 60 - cap$second[ndx] <- secs[ndx] %% 60 - } - } - cap$frac <- NULL - - # Convert NA time parts to 0 - in CF default time is 00:00:00 when not specified - cap$hour[is.na(cap$hour)] <- 0 - cap$minute[is.na(cap$minute)] <- 0 - cap$second[is.na(cap$second)] <- 0 - - # Set timezone to default value where needed - ndx <- which(cap$tz_sign == "Z") - if (length(ndx) > 0) { - cap$tz_sign[ndx] <- "+" - cap$tz_hour[ndx] <- "00" - cap$tz_min[ndx] <- "00" - } - cap$tz <- paste0(ifelse(cap$tz_sign == "", "+", cap$tz_sign), - ifelse(cap$tz_hour == "", "00", cap$tz_hour), - ifelse(cap$tz_min == "", "00", cap$tz_min)) - cap$tz_sign <- cap$tz_hour <- cap$tz_min <- NULL - - # Set optional date parts to 1 if not specified - cap$month[is.na(cap$month)] <- 1 - cap$day[is.na(cap$day)] <- 1 - - # Check date validity - invalid <- mapply(function(y, m, d) {!.is_valid_calendar_date(y, m, d, datum@cal_id)}, - cap$year, cap$month, cap$day) - if (nrow(datum@origin) > 0) { - earlier <- mapply(function(y, m, d, dy, dm, dd) { - if (is.na(y)) return(TRUE) - if (y < dy) return(TRUE) - if (y == dy){ - if (m < dm) return(TRUE) - if (m == dm && d < dd) return(TRUE) - } - return(FALSE) - }, cap$year, cap$month, cap$day, datum@origin[1, 1], datum@origin[1, 2], datum@origin[1, 3]) - invalid <- invalid | earlier - } - if (sum(invalid) > 0) cap[invalid,] <- rep(NA, 7) - - # Calculate offsets - if (nrow(datum@origin) == 0) { # if there's no datum yet, don't calculate offsets - cap$offset <- rep(0, nrow(cap)) # this happens, f.i., when a CFdatum is created - } else { - days <- switch(datum@cal_id, - .date2offset_standard(cap, datum@origin), - .date2offset_julian(cap, datum@origin), - .date2offset_360day(cap, datum@origin), - .date2offset_365day(cap, datum@origin), - .date2offset_366day(cap, datum@origin) - ) - cap$offset <- round((days * 86400 + (cap$hour - datum@origin$hour[1]) * 3600 + - (cap$minute - datum@origin$minute[1]) * 60 + - cap$second - datum@origin$second) / CFt$units$seconds[datum@unit], 3) - } - return(cap) -} - -#' Calculate difference in days between a data.frame of time parts and a datum -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param x data.frame. Dates to calculate the difference for. -#' @param origin data.frame. The origin to calculate the difference against. -#' -#' @returns Vector of days between `x` and the `origin`, using the `standard` calendar. -#' @noRd -.date2offset_standard <- function(x, origin) { - yd0 <- c(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334) # days diff of 1st of month to 1 January in normal year - - datum_year <- origin[1, 1] - datum_days_in_year <- yd0[origin[1, 2]] + origin[1, 3] - if ((origin[1, 2] <= 2) && ((datum_year %% 4 == 0 && datum_year %% 100 > 0) || datum_year %% 400 == 0)) - datum_days_in_year <- datum_days_in_year - 1 - - mapply(function(y, m, d) { - if (is.na(y)) return(NA_integer_) - if (m <= 2 && ((y %% 4 == 0 && y %% 100 > 0) || y %% 400 == 0)) days <- -1 else days <- 0 # -1 if in a leap year up to the leap day, 0 otherwise - repeat { - if (y > datum_year) { - days <- days + 365 + as.integer((y %% 4 == 0 && y %% 100 > 0) || y %% 400 == 0) - y <- y - 1 - } else break - } - days + yd0[m] + d - datum_days_in_year - }, x$year, x$month, x$day) -} - -#' Calculate difference in days between a data.frame of time parts and a datum -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param x data.frame. Dates to calculate the difference for. -#' @param origin data.frame. The origin to calculate the difference against. -#' -#' @returns Vector of days between `x` and the `origin`, using the `julian` calendar. -#' @noRd -.date2offset_julian <- function(x, origin) { - yd0 <- c(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334) # days diff of 1st of month to 1 January in normal year - - datum_year <- origin[1, 1] - datum_days_in_year <- yd0[origin[1, 2]] + origin[1, 3] - if (origin[1, 2] <= 2 && datum_year %% 4 == 0) - datum_days_in_year <- datum_days_in_year - 1 - - mapply(function(y, m, d) { - if (is.na(y)) return(NA_integer_) - if (m <= 2 && y %% 4 == 0) days <- -1 else days <- 0 # -1 if in a leap year up to the leap day, 0 otherwise - repeat { - if (y > datum_year) { - days <- days + 365 + as.integer(y %% 4 == 0) - y <- y - 1 - } else break - } - days + yd0[m] + d - datum_days_in_year - }, x$year, x$month, x$day) -} - -#' Calculate difference in days between a data.frame of time parts and a datum -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param x data.frame. Dates to calculate the difference for. -#' @param origin data.frame. The origin to calculate the difference against. -#' -#' @returns Vector of days between `x` and the `origin`, using the `360_day` calendar. -#' @noRd -.date2offset_360day <- function(x, origin) { - (x$year - origin[1, 1]) * 360 + (x$month - origin[1, 2]) * 30 + x$day - origin[1, 3] -} - -#' Calculate difference in days between a data.frame of time parts and a datum -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param x data.frame. Dates to calculate the difference for. -#' @param origin data.frame. The origin to calculate the difference against. -#' -#' @returns Vector of days between `x` and the `origin`, using the `365_day` calendar. -#' @noRd -.date2offset_365day <- function(x, origin) { - yd0 <- c(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334) # days diff of 1st of month to 1 January - (x$year - origin[1, 1]) * 365 + yd0[x$month] - yd0[origin[1, 2]] + x$day - origin[1, 3] -} - -#' Calculate difference in days between a data.frame of time parts and a datum -#' -#' This is an internal function that should not generally be used outside of -#' the CFtime package. -#' -#' @param x data.frame. Dates to calculate the difference for. -#' @param origin data.frame. The origin to calculate the difference against. -#' -#' @returns Vector of days between `x` and the `origin`, using the `366_day` calendar. -#' @noRd -.date2offset_366day <- function(x, origin) { - yd0 <- c(0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335) # days diff of 1st of month to 1 January - (x$year - origin[1, 1]) * 366 + yd0[x$month] - yd0[origin[1, 2]] + x$day - origin[1, 3] -} diff --git a/R/CFtime-package.R b/R/CFtime-package.R index fabd339..1ab8cfe 100644 --- a/R/CFtime-package.R +++ b/R/CFtime-package.R @@ -10,24 +10,29 @@ #' POSIXt). The CF time coordinate is formally defined in the #' [CF Metadata Conventions document](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#time-coordinate). #' -#' The package can create a `CFtime` instance from scratch or, more commonly, it -#' can use the dimension attributes and dimension variable values from a NetCDF +#' The package can create a [CFTime] instance from scratch or, more commonly, it +#' can use the dimension attributes and dimension variable values from a netCDF #' resource. The package does not actually do any of the reading and the user is -#' free to use their NetCDF package of preference (with the two main options -#' being [RNetCDF](https://cran.r-project.org/package=RNetCDF) and +#' free to use their netCDF package of preference. The recommended package to +#' use (with any netCDF resources) is [ncdfCF](https://cran.r-project.org/package=ncdfCF). +#' `ncdfCF` will automatically use this package to manage the "time" dimension +#' of any netCDF resource. As with this package, it reads and interprets the +#' attributes of the resource to apply the CF Metadata Conventions, supporting +#' axes, auxiliary coordinate variables, coordinate reference systems, etc. +#' Alternatively, for more basic netCDF reading and writing, the two main options +#' are [RNetCDF](https://cran.r-project.org/package=RNetCDF) and #' [ncdf4](https://cran.r-project.org/package=ncdf4)). #' #' **Create, modify, inquire** -#' * [CFtime()]: Create a CFtime instance -#' * [`Properties`][properties] of the CFtime instance -#' * [CFparse()]: Parse a vector of character timestamps into CFtime elements -#' * [`Compare`][CFtime-equivalent] two CFtime instances -#' * [`Merge`][CFtime-merge] two CFtime instances -#' * [`Append`][CFtime-append] additional time steps to a CFtime instance -#' * [as_timestamp()] and [format()]: Generate a vector of character or `POSIXct` timestamps from a CFtime instance +#' * [CFtime()]: Create a [CFTime] instance +#' * [`Properties`][properties] of the `CFTime` instance +#' * [parse_timestamps()]: Parse a vector of character timestamps into `CFTime` elements +#' * [`Compare`][CFtime-equivalent] two `CFTime` instances +#' * [`Merge`][CFtime-merge] two `CFTime` instances or append additional time steps to a `CFTime` instance +#' * [as_timestamp()] and [format()]: Generate a vector of character or `POSIXct` timestamps from a `CFTime` instance #' * [range()]: Timestamps of the two endpoints in the time series -#' * [is_complete()]: Does the CFtime instance have a complete time series between endpoints? -#' * [month_days()]: How many days are there in a month using the CFtime calendar? +#' * [is_complete()]: Does the `CFTime` instance have a complete time series between endpoints? +#' * [month_days()]: How many days are there in a month using the calendar of the `CFTime` instance? #' #' **Factors and coverage** #' * [CFfactor()] and [cut()]: Create factors for different time periods diff --git a/R/CFtime.R b/R/CFtime.R index 67ae06d..76d6b28 100644 --- a/R/CFtime.R +++ b/R/CFtime.R @@ -1,994 +1,837 @@ -#' CF Metadata Conventions time representation +#' @title CFTime class #' -#' @slot datum CFdatum. The origin upon which the `offsets` are based. -#' @slot resolution numeric. The average number of time units between offsets. -#' @slot offsets numeric. A vector of offsets from the datum. -#' @slot bounds Optional, the bounds for the offsets. If not set, it is the -#' logical value `FALSE`. If set, it is the logical value `TRUE` if the bounds -#' are regular with respect to the regularly spaced offsets (e.g. successive -#' bounds are contiguous and at mid-points between the offsets); otherwise a -#' `matrix` with columns for `offsets` and low values in the first row, high -#' values in the second row. +#' @description This class manages the "time" dimension of netCDF files that +#' follow the CF Metadata Conventions, and its productive use in R. #' -#' @returns An object of class CFtime. -#' @export -setClass("CFtime", - slots = c( - datum = "CFdatum", - resolution = "numeric", - offsets = "numeric", - bounds = "ANY" - )) - -#' Create a CFtime object -#' -#' This function creates an instance of the `CFtime` class. The arguments to -#' the call are typically read from a CF-compliant data file with climatological -#' observations or climate projections. Specification of arguments can also be -#' made manually in a variety of combinations. -#' -#' @param definition character. A character string describing the time coordinate -#' of a CF-compliant data file. -#' @param calendar character. A character string describing the calendar to use -#' with the time dimension definition string. Default value is "standard". -#' @param offsets numeric or character, optional. When numeric, a vector of -#' offsets from the origin in the time series. When a character vector, -#' timestamps in ISO8601 or UDUNITS format. When a character string, a -#' timestamp in ISO8601 or UDUNITS format and then a time series will be -#' generated with a separation between steps equal to the unit of measure in -#' the definition, inclusive of the definition timestamp. The unit of measure -#' of the offsets is defined by the time series definition. -#' -#' @returns An instance of the `CFtime` class. -#' @export -#' -#' @examples -#' CFtime("days since 1850-01-01", "julian", 0:364) -#' -#' CFtime("hours since 2023-01-01", "360_day", "2023-01-30T23:00") -CFtime <- function(definition, calendar = "standard", offsets = NULL) { - if (is.null(calendar)) calendar <- "standard" # This may occur when "calendar" attribute is not defined in the NC file - datum <- CFdatum(definition, calendar) - - if (is.array(offsets)) dim(offsets) <- NULL - - if (is.null(offsets)) { - methods::new("CFtime", datum = datum, resolution = NA_real_, offsets = numeric(), bounds = FALSE) - } else if (is.numeric(offsets)) { - stopifnot(.validOffsets(offsets, CFt$units$per_day[datum@unit])) - - if (length(offsets) > 1L) { - resolution <- (max(offsets) - min(offsets)) / (length(offsets) - 1L) - } else { - resolution <- NA_real_ - } - methods::new("CFtime", datum = datum, resolution = resolution, offsets = offsets, bounds = FALSE) - } else if (is.character(offsets)) { - time <- .parse_timestamp(datum, offsets) - if (anyNA(time$year)) stop("Offset argument contains invalid timestamps") - - if (length(offsets) == 1L) { - off <- seq(0L, time$offset[1L]) - resolution <- 1 - } else { - off <- time$offset - resolution <- (max(time$offset) - min(time$offset)) / (length(time$offset) - 1L) - } - methods::new("CFtime", datum = datum, resolution = resolution, offsets = off, bounds = FALSE) - } else stop("Invalid offsets for CFtime object") -} - -#' @aliases properties -#' @title Properties of a CFtime object -#' -#' @description These functions return the properties of an instance of the -#' `CFtime` class. The properties are all read-only, but offsets can be added -#' using the `+` operator. -#' -#' @param cf CFtime. An instance of `CFtime`. -#' -#' @returns `calendar()` and `unit()` return a character string. -#' `origin()` returns a data frame of timestamp elements with a single row -#' of data. `timezone()` returns the datum time zone as a character -#' string. `offsets()` returns a vector of offsets or `NULL` if no offsets -#' have been set. -#' -#' @examples -#' cf <- CFtime("days since 1850-01-01", "julian", 0:364) -#' definition(cf) -#' calendar(cf) -#' unit(cf) -#' timezone(cf) -#' origin(cf) -#' offsets(cf) -#' resolution(cf) - -#' @describeIn properties The definition string of the CFtime instance -#' @export -definition <- function(cf) cf@datum@definition - -#' @describeIn properties The calendar of the CFtime instance -#' @export -calendar <- function(cf) cf@datum@calendar - -#' @describeIn properties The unit of the CFtime instance -#' @export -unit <- function(cf) CFt$units$name[cf@datum@unit] - -#' @describeIn properties The origin of the CFtime instance in timestamp elements -#' @export -origin <- function(cf) cf@datum@origin - -#' @describeIn properties The time zone of the datum of the CFtime instance as a character string -#' @export -timezone <- function(cf) tz(cf@datum) - -#' @describeIn properties The offsets of the CFtime instance as a vector -#' @export -offsets <- function(cf) cf@offsets - -#' @describeIn properties The average separation between the offsets in the CFtime instance -#' @export -resolution <- function(cf) cf@resolution - -#' Bounds of the time offsets -#' -#' CF-compliant NetCDF files store time information as a single offset value for -#' each step along the dimension, typically centered on the valid interval of -#' the data (e.g. 12-noon for day data). Optionally, the lower and upper values -#' of the valid interval are stored in a so-called "bounds" variable, as an -#' array with two rows (lower and higher value) and a column for each offset. -#' With function `bounds()<-` those bounds can be set for a CFtime instance. The -#' bounds can be retrieved with the `bounds()` function. -#' -#' @param x A `CFtime` instance -#' @param format Optional. A single string with format specifiers, see -#' [CFtime::format()] for details. -#' -#' @returns If bounds have been set, an array of bounds values with dimensions -#' (2, length(offsets)). The first row gives the lower bound, the second row -#' the upper bound, with each column representing an offset of `x`. If the -#' `format` argument is specified, the bounds values are returned as strings -#' according to the format. `NULL` when no bounds have been set. -#' @aliases bounds -#' -#' @examples -#' cf <- CFtime("days since 2024-01-01", "standard", seq(0.5, by = 1, length.out = 366)) -#' as_timestamp(cf)[1:3] -#' bounds(cf) <- rbind(0:365, 1:366) -#' bounds(cf)[, 1:3] -#' bounds(cf, "%d-%b-%Y")[, 1:3] -setGeneric("bounds", function(x, format) standardGeneric("bounds"), signature = "x") - -#' @rdname bounds -#' @export -setMethod("bounds", "CFtime", function (x, format) .get_bounds(x, format)) - -#' @rdname bounds -#' @param value A `matrix` (or `array`) with dimensions (2, length(offsets)) -#' giving the lower (first row) and higher (second row) bounds of each offset -#' (this is the format that the CF Metadata Conventions uses for storage in -#' NetCDF files). Use `FALSE` to unset any previously set bounds, `TRUE` to -#' set regular bounds at mid-points between the offsets (which must be regular -#' as well). -setGeneric("bounds<-", function(x, value) standardGeneric("bounds<-"), signature = c("x")) - -#' @rdname bounds -#' @export -setMethod("bounds<-", "CFtime", function (x, value) invisible(.set_bounds(x, value))) - -#' The length of the offsets contained in the CFtime instance. -#' -#' @param x The CFtime instance whose length will be returned -#' -#' @return The number of offsets in the specified CFtime instance. -#' @export -#' -#' @examples -#' cf <- CFtime("days since 1850-01-01", "julian", 0:364) -#' length(cf) -setMethod("length", "CFtime", function(x) length(x@offsets)) - -#' Return the timestamps contained in the CFtime instance. -#' -#' @param x The CFtime instance whose timestamps will be returned +#' The class has a field `cal` which holds a specific calendar from the +#' allowed types (9 named calendars are currently supported). The calendar is +#' also implemented as a (hidden) class which converts netCDF file encodings to +#' timestamps as character strings, and vice-versa. Bounds information (the +#' period of time over which a timestamp is valid) is used when defined in the +#' netCDF file. #' -#' @return The timestamps in the specified CFtime instance. -#' @export -#' -#' @examples -#' cf <- CFtime("days since 1850-01-01", "julian", 0:364) -#' as.character(cf) -setMethod("as.character", "CFtime", function(x) { - if (length(x@offsets) > 0) - as_timestamp(x) -}) - -setMethod("show", "CFtime", function(object) { - noff <- length(object@offsets) - if (noff == 0L) { - el <- " Elements: (no elements)\n" - b <- " Bounds : (not set)\n" - } else { - d <- .ts_extremes(object) - if (noff > 1L) { - el <- sprintf(" Elements: [%s .. %s] (average of %f %s between %d elements)\n", - d[1L], d[2L], object@resolution, CFt$units$name[object@datum@unit], noff) - } else { - el <- paste(" Elements:", d[1L], "\n") - } - if (is.logical(object@bounds)) { - if (object@bounds) b <- " Bounds : regular and consecutive\n" - else b <- " Bounds : not set\n" - } else b <- " Bounds : irregular\n" - } - cat("CF time series:\n", methods::show(object@datum), el, b, sep = "") -}) - -#' Format time elements using format specifiers -#' -#' Format timestamps using a specific format string, using the specifiers -#' defined for the [base::strptime()] function, with limitations. The only -#' supported specifiers are `bBdeFhHIjmMpRSTYz%`. Modifiers `E` and `O` are -#' silently ignored. Other specifiers, including their percent sign, are copied -#' to the output as if they were adorning text. -#' -#' The formatting is largely oblivious to locale. The reason for this is that -#' certain dates in certain calendars are not POSIX-compliant and the system -#' functions necessary for locale information thus do not work consistently. The -#' main exception to this is the (abbreviated) names of months (`bB`), which -#' could be useful for pretty printing in the local language. For separators and -#' other locale-specific adornments, use local knowledge instead of depending on -#' system locale settings; e.g. specify `%m/%d/%Y` instead of `%D`. -#' -#' Week information, including weekday names, is not supported at all as a -#' "week" is not defined for non-standard CF calendars and not generally useful -#' for climate projection data. If you are working with observed data and want -#' to get pretty week formats, use the [as_timestamp()] function to generate -#' `POSIXct` timestamps (observed data generally uses a standard calendar) and -#' then use the [base::format()] function which supports the full set of -#' specifiers. -#' -#' @param x CFtime. A CFtime instance whose offsets will be returned as -#' timestamps. -#' @param format character. A character string with strptime format -#' specifiers. If omitted, the most economical format will be used: a full -#' timestamp when time information is available, a date otherwise. -#' -#' @returns A vector of character strings with a properly formatted timestamp. -#' Any format specifiers not recognized or supported will be returned verbatim. -#' @export -#' -#' @examples -#' cf <- CFtime("days since 2020-01-01", "standard", 0:365) -#' format(cf, "%Y-%b") -#' -#' # Use system facilities on a standard calendar -#' format(as_timestamp(cf, asPOSIX = TRUE), "%A, %x") -#' -setMethod("format", "CFtime", function(x, format) { - if (!requireNamespace("stringr", quietly = TRUE)) - stop("package `stringr` is required - please install it first") # nocov - - if (missing(format)) format <- "" - else if (!is.character(format) || length(format) != 1) - stop("`format` argument must be a character string with formatting specifiers") - - ts <- .offsets2time(x@offsets, x@datum) - if (nrow(ts) == 0L) return() - - .format_format(ts, tz(x@datum), format) -}) - -#' Create a factor for a CFtime instance -#' -#' Method for [base::cut()] applied to CFtime objects. -#' -#' When `breaks` is one of `"year", "season", "quarter", "month", "dekad", -#' "day"` a factor is generated like by [CFfactor()]. -#' -#' When `breaks` is a vector of character timestamps a factor is produced with a -#' level for every interval between timestamps. The last timestamp, therefore, -#' is only used to close the interval started by the pen-ultimate timestamp - -#' use a distant timestamp (e.g. `range(x)[2]`) to ensure that all offsets to -#' the end of the CFtime time series are included, if so desired. The last -#' timestamp will become the upper bound in the CFtime instance that is returned -#' as an attribute to this function so a sensible value for the last timestamp -#' is advisable. The earliest timestamp cannot be earlier than the origin of the -#' datum of `x`. -#' -#' This method works similar to [base::cut.POSIXt()] but there are some -#' differences in the arguments: for `breaks` the set of options is different -#' and no preceding integer is allowed, `labels` are always assigned using -#' values of `breaks`, and the interval is always left-closed. -#' -#' @param x An instance of CFtime. -#' @param breaks A character string of a factor period (see [CFfactor()] for a -#' description), or a character vector of timestamps that conform to the -#' calendar of `x`, with a length of at least 2. Timestamps must be given in -#' ISO8601 format, e.g. "2024-04-10 21:31:43". -#' @param ... Ignored. +#' Additionally, this class has functions to ease use of the netCDF "time" +#' information when processing data from netCDF files. Filtering and indexing of +#' time values is supported, as is the generation of factors. #' -#' @returns A factor with levels according to the `breaks` argument, with -#' attributes 'period', 'epoch' and 'CFtime'. When `breaks` is a factor -#' period, attribute 'period' has that value, otherwise it is '"day"'. When -#' `breaks` is a character vector of timestamps, attribute 'CFtime' holds an -#' instance of CFtime that has the same definition as `x`, but with (ordered) -#' offsets generated from the `breaks`. Attribute 'epoch' is always -1. -#' @aliases cut -#' @seealso [CFfactor()] produces a factor for several fixed periods, including -#' for epochs. #' @export -#' -#' @examples -#' x <- CFtime("days since 2021-01-01", "365_day", 0:729) -#' breaks <- c("2022-02-01", "2021-12-01", "2023-01-01") -#' cut(x, breaks) -setMethod("cut", "CFtime", function (x, breaks, ...) { - if (!inherits(x, "CFtime")) - stop("Argument 'x' must be a CFtime instance") - - if (missing(breaks) || !is.character(breaks) || (len <- length(breaks)) < 1) - stop("Argument 'breaks' must be a character vector with at least 1 value") - - if(len == 1) { - breaks <- sub("s$", "", tolower(breaks)) - if (breaks %in% CFt$factor_periods) - return(CFfactor(x, breaks)) - else stop("Invalid specification of 'breaks'") - } - - # breaks is a character vector of multiple timestamps - if (x@datum@unit > 4L) stop("Factorizing on a 'month' or 'year' datum is not supported") - time <- .parse_timestamp(x@datum, breaks) - if (anyNA(time$year)) - stop("Invalid specification of 'breaks'") - sorted <- order(time$offset) - ooff <- time$offset[sorted] - intv <- findInterval(offsets(x), ooff) - intv[which(intv %in% c(0L, len))] <- NA - f <- factor(intv, labels = breaks[sorted][1L:(len-1L)]) - - # Attributes - bnds <- rbind(ooff[1L:(len-1L)], ooff[2L:len]) - off <- bnds[1L, ] + (bnds[2L, ] - bnds[1L, ]) * 0.5 - cf <- CFtime(x@datum@definition, x@datum@calendar, off) - bounds(cf) <- bnds - attr(f, "period") <- "day" - attr(f, "epoch") <- -1L - attr(f, "CFtime") <- cf - f -}) - -setGeneric("indexOf", function(x, y, ...) standardGeneric("indexOf"), signature = c("x", "y")) - -#' Find the index of timestamps in the time series -#' -#' In the CFtime instance `y`, find the index in the time series for each -#' timestamp given in argument `x`. Values of `x` that are before the earliest -#' value in `y` will be returned as `0` (except when the value is before the -#' datum of `y`, in which case the value returned is `NA`); values of `x` that -#' are after the latest values in `y` will be returned as -#' `.Machine$integer.max`. Alternatively, when `x` is a numeric vector of index -#' values, return the valid indices of the same vector, with the side effect -#' being the attribute "CFtime" associated with the result. -#' -#' Timestamps can be provided as vectors of character strings, `POSIXct` or -#' `Date.` -#' -#' Matching also returns index values for timestamps that fall between two -#' elements of the time series - this can lead to surprising results when time -#' series elements are positioned in the middle of an interval (as the CF -#' Metadata Conventions instruct us to "reasonably assume"): a time series of -#' days in January would be encoded in a netCDF file as -#' `c("2024-01-01 12:00:00", "2024-01-02 12:00:00", "2024-01-03 12:00:00", ...)` -#' so `x <- c("2024-01-01", "2024-01-02", "2024-01-03")` would result in -#' `(NA, 1, 2)` (or `(NA, 1.5, 2.5)` with `method = "linear"`) because the date -#' values in `x` are at midnight. This situation is easily avoided by ensuring -#' that `y` has bounds set (use `bounds(y) <- TRUE` as a proximate solution if -#' bounds are not stored in the netCDF file). See the Examples. -#' -#' If bounds are set, the indices are taken from those bounds. Returned indices -#' may fall in between bounds if the latter are not contiguous, with the -#' exception of the extreme values in `x`. -#' -#' Values of `x` that are not valid timestamps according to the calendar of `y` -#' will be returned as `NA`. -#' -#' `x` can also be a numeric vector of index values, in which case the valid -#' values in `x` are returned. If negative values are passed, the positive -#' counterparts will be excluded and then the remainder returned. Positive and -#' negative values may not be mixed. Using a numeric vector has -#' the side effect that the result has the attribute "CFtime" describing the -#' temporal dimension of the slice. If index values outside of the range of `y` -#' (`1:length(y)`) are provided, an error will be thrown. -#' -#' @param x Vector of character, POSIXt or Date values to find indices for, or a -#' numeric vector. -#' @param y CFtime instance. -#' @param method Single value of "constant" or "linear". If `"constant"` or when -#' bounds are set on argument `y`, return the index value for each match. If -#' `"linear"`, return the index value with any fractional value. -#' -#' @returns A numeric vector giving indices into the "time" dimension of the -#' dataset associated with `y` for the values of `x`. If there is at least 1 -#' valid index, then attribute "CFtime" -#' contains an instance of CFtime that describes the dimension of filtering -#' the dataset associated with `y` with the result of this function, excluding -#' any `NA`, `0` and `.Machine$integer.max` values. -#' @aliases indexOf -#' @export -#' -#' @examples -#' cf <- CFtime("days since 2020-01-01", "360_day", 1440:1799 + 0.5) -#' as_timestamp(cf)[1:3] -#' x <- c("2024-01-01", "2024-01-02", "2024-01-03") -#' indexOf(x, cf) -#' indexOf(x, cf, method = "linear") -#' -#' bounds(cf) <- TRUE -#' indexOf(x, cf) -#' -#' # Non-existent calendar day in a `360_day` calendar -#' x <- c("2024-03-30", "2024-03-31", "2024-04-01") -#' indexOf(x, cf) -#' -#' # Numeric x -#' indexOf(c(29, 30, 31), cf) -setMethod("indexOf", c("ANY", "CFtime"), function(x, y, method = "constant") { - stopifnot(inherits(x, c("character", "POSIXt", "Date")) || is.numeric(x), - method %in% c("constant", "linear")) - - if (is.numeric(x)) { - if (!(all(x < 0, na.rm = TRUE) || all(x > 0, na.rm = TRUE))) - stop("Cannot mix positive and negative index values") - - intv <- (1:length(y))[x] - xoff <- y@offsets[x] - } else { - if (y@datum@unit > 4L) - stop("Parsing of timestamps on a \"month\" or \"year\" datum is not supported.") - - xoff <- .parse_timestamp(y@datum, as.character(x))$offset - vals <- .get_bounds(y) - if (is.null(vals)) vals <- offsets(y) - else vals <- c(vals[1L, 1L], vals[2L, ]) - intv <- stats::approx(vals, 1L:length(vals), xoff, method = method, - yleft = 0, yright = .Machine$integer.max)$y - intv[which(intv == length(vals))] <- .Machine$integer.max - } - - valid <- which(!is.na(intv) & intv > 0 & intv < .Machine$integer.max) - if (any(valid)) { - cf <- CFtime(definition(y), calendar(y), xoff[valid]) - yb <- bounds(y) - if (!is.null(yb)) - bounds(cf) <- yb[, intv[valid], drop = FALSE] - attr(intv, "CFtime") <- cf - } - intv -}) - -#' @title Extreme time series values -#' -#' @description Character representation of the extreme values in the time series -#' -#' @param x An instance of the `CFtime` class. -#' @param format A character string with format specifiers, optional. If it is -#' missing or an empty string, the most economical ISO8601 format is chosen: -#' "date" when no time information is present in `x`, "timestamp" otherwise. -#' Otherwise a suitable format specifier can be provided. -#' @param bounds Logical to indicate if the extremes from the bounds should be -#' used, if set. Defaults to `FALSE`. -#' @param ... Ignored. -#' @param na.rm Ignored. -#' -#' @returns Vector of two character representations of the extremes of the time series. -#' @export -#' @examples -#' cf <- CFtime("days since 1850-01-01", "julian", 0:364) -#' range(cf) -#' range(cf, "%Y-%b-%e") -setMethod("range", "CFtime", function(x, format = "", bounds = FALSE, ..., na.rm = FALSE) - .ts_extremes(x, format, bounds, ..., na.rm)) - -#' Indicates if the time series is complete -#' -#' This function indicates if the time series is complete, meaning that the time -#' steps are equally spaced and there are thus no gaps in the time series. -#' -#' This function gives exact results for time series where the nominal -#' *unit of separation* between observations in the time series is exact in terms of the -#' datum unit. As an example, for a datum unit of "days" where the observations -#' are spaced a fixed number of days apart the result is exact, but if the same -#' datum unit is used for data that is on a monthly basis, the *assessment* is -#' approximate because the number of days per month is variable and dependent on -#' the calendar (the exception being the `360_day` calendar, where the -#' assessment is exact). The *result* is still correct in most cases (including -#' all CF-compliant data sets that the developers have seen) although there may -#' be esoteric constructions of CFtime and offsets that trip up this -#' implementation. -#' -#' @param x An instance of the `CFtime` class -#' -#' @returns logical. `TRUE` if the time series is complete, with no gaps; -#' `FALSE` otherwise. If no offsets have been added to the CFtime instance, -#' `NA` is returned. -#' @export -#' @examples -#' cf <- CFtime("days since 1850-01-01", "julian", 0:364) -#' is_complete(cf) -is_complete <- function(x) { - if (!methods::is(x, "CFtime")) stop("Argument must be an instance of CFtime") - if (length(x@offsets) == 0L) NA - else .ts_equidistant(x) -} - -#' Which time steps fall within two extreme values -#' -#' Given two extreme character timestamps, return a logical vector of a length -#' equal to the number of time steps in the CFtime instance with values `TRUE` -#' for those time steps that fall between the two extreme values, `FALSE` -#' otherwise. This can be used to select slices from the time series in reading -#' or analysing data. -#' -#' If bounds were set these will be preserved. -#' -#' @param x CFtime. The time series to operate on. -#' @param extremes character. Vector of two timestamps that represent the -#' extremes of the time period of interest. The timestamps must be in -#' increasing order. The timestamps need not fall in the range of the time -#' steps in the CFtime stance. -#' @param rightmost.closed Is the larger extreme value included in the result? -#' Default is `FALSE`. -#' -#' @returns A logical vector with a length equal to the number of time steps in -#' `x` with values `TRUE` for those time steps that fall between the two -#' extreme values, `FALSE` otherwise. The earlier timestamp is included, the -#' later timestamp is excluded. A specification of `c("2022-01-01", "2023-01-01")` -#' will thus include all time steps that fall in the year 2022. -#' @export -#' -#' @examples -#' cf <- CFtime("hours since 2023-01-01 00:00:00", "standard", 0:23) -#' slab(cf, c("2022-12-01", "2023-01-01 03:00")) -slab <- function(x, extremes, rightmost.closed = FALSE) { - if (!methods::is(x, "CFtime")) stop("First argument must be an instance of CFtime") - if (!is.character(extremes) || length(extremes) != 2L) - stop("Second argument must be a character vector of two timestamps") - if (extremes[2L] < extremes[1L]) extremes <- c(extremes[2L], extremes[1L]) - .ts_slab(x, extremes, rightmost.closed) -} - -#' Equivalence of CFtime objects -#' -#' This operator can be used to test if two `CFtime` objects represent the same -#' CF-convention time coordinates. Two `CFtime` objects are considered equivalent -#' if they have an equivalent datum and the same offsets. -#' -#' @param e1,e2 CFtime. Instances of the `CFtime` class. -#' -#' @returns `TRUE` if the `CFtime` objects are equivalent, `FALSE` otherwise. -#' @export -#' @aliases CFtime-equivalent -#' -#' @examples -#' e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) -#' e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 0:364) -#' e1 == e2 -setMethod("==", c("CFtime", "CFtime"), function(e1, e2) - .datum_equivalent(e1@datum, e2@datum) && - length(e1@offsets) == length(e2@offsets) && - all(e1@offsets == e2@offsets)) - -#' Merge two CFtime objects -#' -#' Two `CFtime` instances can be merged into one with this operator, provided -#' that the units and calendars of the datums of the two instances are -#' equivalent. -#' -#' If the origins of the two datums are not identical, the earlier origin is -#' preserved and the offsets of the later origin are updated in the resulting -#' CFtime instance. -#' -#' The order of the two parameters is indirectly significant. The resulting -#' `CFtime` instance will have the offsets of both instances in the order that -#' they are specified. There is no reordering or removal of duplicates. This is -#' because the time series are usually associated with a data set and the -#' correspondence between the data in the files and the CFtime instance is thus -#' preserved. When merging the data sets described by this time series, the -#' order must be identical to the merging here. -#' -#' Any bounds that were set will be removed. Use [CFtime::bounds()] to retrieve -#' the bounds of the individual `CFtime` instances and then set them again after -#' merging the two instances. -#' -#' @param e1,e2 CFtime. Instances of the `CFtime` class. -#' -#' @returns A `CFtime` object with a set of offsets composed of the offsets of -#' the instances of `CFtime` that the operator operates on. If the datum units -#' or calendars of the `CFtime` instances are not equivalent, an error is -#' thrown. -#' @export -#' @aliases CFtime-merge -#' -#' @examples -#' e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) -#' e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 365:729) -#' e1 + e2 -setMethod("+", c("CFtime", "CFtime"), function(e1, e2) { - if (!.datum_compatible(e1@datum, e2@datum)) stop('Datums not compatible') - if (all(e1@datum@origin[1:6] == e2@datum@origin[1:6])) - CFtime(e1@datum@definition, e1@datum@calendar, c(e1@offsets, e2@offsets)) - else { - diff <- .parse_timestamp(e1@datum, paste(origin_date(e2@datum), origin_time(e2@datum)))$offset - if (is.na(diff)) { - diff <- .parse_timestamp(e2@datum, paste(origin_date(e1@datum), origin_time(e1@datum)))$offset - CFtime(e2@datum@definition, e2@datum@calendar, c(e1@offsets + diff, e2@offsets)) - } else - CFtime(e1@datum@definition, e1@datum@calendar, c(e1@offsets, e2@offsets + diff)) - } -}) - -#' Extend a CFtime object with additional offsets -#' -#' A `CFtime` instance can be extended by adding additional offsets using this -#' operator. -#' -#' The resulting `CFtime` instance will have its offsets in the order that they -#' are added, meaning that the offsets from the `CFtime` instance come first and -#' those from the numeric vector follow. There is no reordering or removal of -#' duplicates. This is because the time series are usually associated with a -#' data set and the correspondence between the two is thus preserved, if and -#' only if the data sets are merged in the same order. -#' -#' Note that when adding multiple vectors of offsets to a `CFtime` instance, it -#' is more efficient to first concatenate the vectors and then do a final -#' addition to the `CFtime` instance. So avoid `CFtime(definition, calendar, e1) + CFtime(definition, calendar, e2) + CFtime(definition, calendar, e3) + ...` -#' but rather do `CFtime(definition, calendar) + c(e1, e2, e3, ...)`. It is the -#' responsibility of the operator to ensure that the offsets of the different -#' data sets are in reference to the same datum. -#' -#' Note also that `RNetCDF` and `ncdf4` packages both return the values of the -#' "time" dimension as a 1-dimensional array. You have to `dim(time_values) <- NULL` -#' to de-class the array to a vector before adding offsets to an existing CFtime -#' instance. -#' -#' Negative offsets will generate an error. -#' -#' Any bounds that were set will be removed. Use [CFtime::bounds()] to retrieve -#' the bounds of the individual `CFtime` instances and then set them again after -#' merging the two instances. -#' -#' @param e1 CFtime. Instance of the `CFtime` class. -#' @param e2 numeric. Vector of offsets to be added to the `CFtime` instance. -#' -#' @returns A `CFtime` object with offsets composed of the `CFtime` instance and -#' the numeric vector. -#' @export -#' @aliases CFtime-append -#' -#' @examples -#' e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) -#' e2 <- 365:729 -#' e1 + e2 -setMethod("+", c("CFtime", "numeric"), function(e1, e2) { - if (.validOffsets(e2, CFt$units$per_day[e1@datum@unit])) - CFtime(e1@datum@definition, e1@datum@calendar, c(e1@offsets, e2)) -}) - -#' Validate offsets passed into a CFtime instance -#' -#' This is an internal function that should not be used outside the CFtime -#' package. -#' -#' Tests the `offsets` values. Throws an error if the argument contains negative or `NA` values. -#' -#' @param offsets The offsets to test -#' -#' @returns logical. `TRUE` if the offsets are valid, throws an error otherwise. -#' @noRd -.validOffsets <- function(offsets, upd) { - if (any(is.na(offsets) | (offsets < 0))) stop("Offsets cannot contain negative or `NA` values.") - if (any(offsets > 1000000 * upd)) stop("Offset values are outside of reasonable range (year 1 - 2500).") - TRUE -} - -#' Return the extremes of the time series as character strings -#' -#' This function returns the first and last timestamp of the time series as a -#' vector. Note that the offsets do not have to be sorted. -#' -#' This is an internal function that should not be used outside of the CFtime -#' package. -#' -#' @param x CFtime. The time series to operate on. -#' @param format character. Value of "date" or "timestamp". Optionally, a -#' character string that specifies an alternate format. -#' -#' @returns Vector of two character strings that represent the starting and -#' ending timestamps in the time series. If a `format` is supplied, that -#' format will be used. Otherwise, if all of the timestamps in the time series -#' have a time component of `00:00:00` the date of the timestamp is returned, -#' otherwise the full timestamp (without any time zone information). -#' -#' @noRd -.ts_extremes <- function(x, format = "", bounds = FALSE, ..., na.rm) { - if (length(x@offsets) == 0L) return(c(NA_character_, NA_character_)) - if (!missing(format) && ((!is.character(format)) || length(format) != 1L)) - stop("`format` argument, when present, must be a character string with formatting specifiers") - if (!is.logical(bounds) || length(bounds) != 1L) - stop("`bounds` argument, when present, must be a single logical value") - - if (bounds) { - bnds <- .get_bounds(x) - if (is.null(bnds)) time <- .offsets2time(range(x@offsets), x@datum) - else time <- .offsets2time(c(bnds[1L, 1L], bnds[2L, length(x)]), x@datum) - } else time <- .offsets2time(range(x@offsets), x@datum) - - .format_format(time, tz(x@datum), format) -} - -#' Which time steps fall within two extreme values -#' -#' Given two extreme character timestamps, return a logical vector of a length -#' equal to the number of time steps in the CFtime instance with values `TRUE` -#' for those time steps that fall between the two extreme values, `FALSE` -#' otherwise. -#' -#' **NOTE** Giving crap as the earlier timestamp will set that value to 0. So -#' invalid input will still generate a result. To be addressed. Crap in later -#' timestamp is not tolerated. -#' -#' @param x CFtime. The time series to operate on. -#' @param extremes character. Vector of two timestamps that represent the -#' extremes of the time period of interest. The timestamps must be in -#' increasing order. -#' @param closed Is the right side closed, i.e. included in the result? -#' -#' @returns A logical vector with a length equal to the number of time steps in -#' `x` with values `TRUE` for those time steps that fall between the two -#' extreme values, `FALSE` otherwise. The earlier timestamp is included, the -#' later timestamp is excluded. A specification of `c("2022-01-01", "2023-01-01)` -#' will thus include all time steps that fall in the year 2022. -#' -#' An attribute 'CFtime' will have the same definition as `x` but with offsets -#' corresponding to the time steps falling between the two extremes. If there -#' are no values between the extremes, the attribute is `NULL`. -#' @noRd -.ts_slab <- function(x, extremes, closed) { - ext <- .parse_timestamp(x@datum, extremes)$offset - if (is.na(ext[1L])) ext[1L] <- 0 - off <- x@offsets - if (ext[1L] > max(off) || is.na(ext[2L])) { - out <- rep(FALSE, length(off)) - attr(out, "CFtime") <- NULL - } else { - out <- if (closed) off >= ext[1L] & off <= ext[2L] - else off >= ext[1L] & off < ext[2L] - cf <- CFtime(x@datum@definition, x@datum@calendar, off[out]) - xb <- bounds(x) - if (!is.null(xb)) - bounds(cf) <- xb[, out] - attr(out, "CFtime") <- cf - } - out -} - -#' Decompose a vector of offsets, in units of the datum, to their timestamp -#' values -#' -#' This function adds a specified amount of time to the origin of a CFts object. -#' -#' This is an internal function that should not be used outside of the CFtime -#' package. -#' -#' This functions may introduce inaccuracies where the datum unit is "months" or -#' "years", due to the ambiguous definition of these units. -#' -#' @param offsets numeric. Vector of offsets to add to the datum. -#' @param datum CFdatum. The datum that defines the unit of the offsets and the -#' origin to add the offsets to. -#' -#' @returns A data.frame with columns for the timestamp elements and as many -#' rows as there are offsets. -#' @noRd -.offsets2time <- function(offsets, datum) { - len <- length(offsets) - if(len == 0L) return(data.frame(year = integer(), month = integer(), day = integer(), - hour = integer(), minute = integer(), second = numeric(), - tz = character(), offset = numeric())) - - if (datum@unit <= 4L) { # Days, hours, minutes, seconds - # First add time: convert to seconds first, then recompute time parts - secs <- offsets * CFt$units$seconds[datum@unit] - secs <- secs + datum@origin$hour[1L] * 3600L + datum@origin$minute[1L] * 60L + datum@origin$second[1L] - days <- secs %/% 86400L # overflow days - secs <- round(secs %% 86400L, 3L) # drop overflow days from time, round down to milli-seconds avoid errors - - # Time elements for output - hrs <- secs %/% 3600L - mins <- (secs %% 3600L) %/% 60L - secs <- secs %% 60L - - # Now add days using the calendar of the datum - origin <- unlist(datum@origin[1L,1L:3L]) # origin ymd as a named vector - if (any(days > 0)) { - switch (datum@cal_id, - out <- .offset2date_standard(days, origin), - out <- .offset2date_julian(days, origin), - out <- .offset2date_360(days, origin), - out <- .offset2date_fixed(days, origin, c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), 365), - out <- .offset2date_fixed(days, origin, c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), 366)) - } else { - out <- data.frame(year = rep(origin[1L], len), month = rep(origin[2L], len), day = rep(origin[3L], len)) - } - - # Put it all back together again - out$hour <- hrs - out$minute <- mins - out$second <- secs - out$tz <- rep(tz(datum), len) - } else { # Months, years - out <- datum@origin[rep(1L, len), ] - if (datum@unit == 5L) { # Offsets are months - months <- out$month + offsets - 1L - out$month <- months %% 12L + 1L - out$year <- out$year + months %/% 12L - } else { # Offsets are years - out$year <- out$year + offsets - } - } - out$offset <- offsets - return(out) -} - -#' 360_day, use integer arithmetic -#' This is an internal function that should not be used outside of the CFtime package. -#' -#' @param x integer. Vector of days to add to the origin. -#' @param origin integer. Vector of year, month, day and seconds to add days to. -#' -#' @returns A data frame with time elements year, month and day in columns and as -#' many rows as the length of vector `x`. -#' @noRd -.offset2date_360 <- function(x, origin) { - y <- origin[1L] + x %/% 360L - m <- origin[2L] + (x %% 360L) %/% 30L - d <- origin[3L] + x %% 30L - over <- which(d > 30L) - d[over] <- d[over] - 30L - m[over] <- m[over] + 1L - over <- which(m > 12L) - m[over] <- m[over] - 12L - y[over] <- y[over] + 1L - data.frame(year = y, month = m, day = d, row.names = NULL) -} - -#' Fixed year length, either 365_day or 366_day -#' -#' This is an internal function that should not be used outside of the CFtime package. -#' -#' @param x numeric. Vector of days to add to the origin. -#' @param origin numeric. Vector of year, month, day and seconds to add days to. -#' @param month numeric. Vector of days per month in the year. -#' @param ydays numeric. Number of days per year, either 365 or 366. -#' -#' @returns A data frame with time elements year, month and day in columns and as -#' many rows as the length of vector `x`. -#' @noRd -.offset2date_fixed <- function(x, origin, month, ydays) { - # First process full years over the vector - yr <- origin[1L] + (x %/% ydays) - x <- x %% ydays - - # Remaining portion per datum - x <- x + origin[3L] - ymd <- mapply(function(y, m, d) { - while (d > month[m]) { - d <- d - month[m] - m <- m + 1L - if (m == 13L) { - y <- y + 1L - m <- 1L +#' @references +#' https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#time-coordinate +#' @docType class +CFTime <- R6::R6Class("CFTime", + public = list( + #' @field cal The calendar of this `CFTime` instance, a descendant of the + #' [CFCalendar] class. + cal = NULL, + + #' @field offsets A numeric vector of offsets from the origin of the + #' calendar. + offsets = numeric(), + + #' @field resolution The average number of time units between offsets. + resolution = NA_real_, + + #' @field bounds Optional, the bounds for the offsets. If not set, it is the + #' logical value `FALSE`. If set, it is the logical value `TRUE` if the + #' bounds are regular with respect to the regularly spaced offsets (e.g. + #' successive bounds are contiguous and at mid-points between the + #' offsets); otherwise a `matrix` with columns for `offsets` and low + #' values in the first row, high values in the second row. + bounds = FALSE, + + #' @description Create a new instance of this class. + #' @param definition Character string of the units and origin of the + #' calendar. + #' @param calendar Character string of the calendar to use. Must be one of + #' the values permitted by the CF Metadata Conventions. If `NULL`, the + #' "standard" calendar will be used. + #' @param offsets Numeric or character vector, optional. When numeric, a + #' vector of offsets from the origin in the time series. When a character + #' vector of length 2 or more, timestamps in ISO8601 or UDUNITS format. + #' When a character string, a timestamp in ISO8601 or UDUNITS format and + #' then a time series will be generated with a separation between steps + #' equal to the unit of measure in the definition, inclusive of the + #' definition timestamp. The unit of measure of the offsets is defined by + #' the `definition` argument. + initialize = function(definition, calendar, offsets) { + if (is.null(calendar)) calendar <- "standard" # This may occur when "calendar" attribute is not defined in the NC file + self$cal <- switch(calendar, + "standard" = CFCalendarStandard$new(calendar, definition), + "gregorian" = CFCalendarStandard$new(calendar, definition), + "proleptic_gregorian" = CFCalendarProleptic$new(calendar, definition), + "julian" = CFCalendarJulian$new(calendar, definition), + "360_day" = CFCalendar360$new(calendar, definition), + "365_day" = CFCalendar365$new(calendar, definition), + "noleap" = CFCalendar365$new(calendar, definition), + "366_day" = CFCalendar366$new(calendar, definition), + "all_leap" = CFCalendar366$new(calendar, definition), + stop("Invalid calendar specification", call. = FALSE) + ) + + if (is.numeric(offsets)) { + dim(offsets) <- NULL + stopifnot(.validOffsets(offsets)) + + if (length(offsets) > 1L) { + self$resolution <- (max(offsets) - min(offsets)) / (length(offsets) - 1L) + if (any(diff(offsets) <= 0)) + warning("Offsets not monotonically increasing.", call. = FALSE) + } else { + self$resolution <- NA_real_ + } + self$offsets <- offsets + } else if (is.character(offsets)) { + time <- self$cal$parse(offsets) + if (anyNA(time$year)) stop("Argument `offsets` contains invalid timestamps", call. = FALSE) + + if (length(offsets) == 1L) { + self$offsets <- seq(0L, time$offset[1L]) + self$resolution <- 1 + } else { + self$offsets <- time$offset + self$resolution <- (max(self$offsets) - min(self$offsets)) / (length(self$offsets) - 1L) + if (any(diff(self$offsets) <= 0)) + warning("Offsets not monotonically increasing.", call. = FALSE) + } + } else if (!is.null(offsets)) stop("Invalid offsets for CFTime object", call. = FALSE) + }, + + #' @description Print a summary of the `CFTime` object to the console. + #' @param ... Ignored. + #' @return `self` invisibly. + print = function(...) { + noff <- length(self$offsets) + if (noff == 0L) { + el <- " Elements: (no elements)\n" + b <- " Bounds : (not set)\n" + } else { + d <- self$range() + if (noff > 1L) { + el <- sprintf(" Elements: [%s .. %s] (average of %f %s between %d elements)\n", + d[1L], d[2L], self$resolution, CFt$units$name[self$cal$unit], noff) + } else { + el <- paste(" Elements:", d[1L], "\n") + } + if (is.logical(self$bounds)) { + if (self$bounds) b <- " Bounds : regular and consecutive\n" + else b <- " Bounds : not set\n" + } else b <- " Bounds : irregular\n" + } + cal <- capture.output(self$cal$print()) + cat(paste(cal, collapse = "\n"), "\nTime series:\n", el, b, sep = "") + invisible(self) + }, + + #' @description This method returns the first and last timestamp of the time + #' series as a vector. Note that the offsets do not have to be sorted. + #' + #' @param format Value of "date" or "timestamp". Optionally, a + #' character string that specifies an alternate format. + #' @param bounds Logical to indicate if the extremes from the bounds should + #' be used, if set. Defaults to `FALSE`. + #' + #' @return Vector of two character strings that represent the starting and + #' ending timestamps in the time series. If a `format` is supplied, that + #' format will be used. Otherwise, if all of the timestamps in the time + #' series have a time component of `00:00:00` the date of the timestamp is + #' returned, otherwise the full timestamp (without any time zone + #' information). + range = function(format = "", bounds = FALSE) { + if (length(self$offsets) == 0L) return(c(NA_character_, NA_character_)) + if (!missing(format) && ((!is.character(format)) || length(format) != 1L)) + stop("`format` argument, when present, must be a character string with formatting specifiers", call. = FALSE) + if (!is.logical(bounds) || length(bounds) != 1L) + stop("`bounds` argument, when present, must be a single logical value", call. = FALSE) + + if (bounds) { + bnds <- self$get_bounds() + if (is.null(bnds)) time <- self$cal$offsets2time(base::range(self$offsets)) + else time <- self$cal$offsets2time(c(bnds[1L, 1L], bnds[2L, length(self$offsets)])) + } else time <- self$cal$offsets2time(base::range(self$offsets)) + + .format_format(time, self$cal$timezone, format) + }, + + #' @description This method generates a vector of character strings or + #' `POSIXct`s that represent the date and time in a selectable combination + #' for each offset. + #' + #' The character strings use the format `YYYY-MM-DDThh:mm:ss±hhmm`, + #' depending on the `format` specifier. The date in the string is not + #' necessarily compatible with `POSIXt` - in the `360_day` calendar + #' `2017-02-30` is valid and `2017-03-31` is not. + #' + #' For the "proleptic_gregorian" calendar the output can also be generated + #' as a vector of `POSIXct` values by specifying `asPOSIX = TRUE`. The + #' same is possible for the "standard" and "gregorian" calendars but only + #' if all timestamps fall on or after 1582-10-15. If `asPOSIX = TRUE` is + #' specified while the calendar does not support it, an error will be + #' generated. + #' + #' @param format character. A character string with either of the values + #' "date" or "timestamp". If the argument is not specified, the format + #' used is "timestamp" if there is time information, "date" otherwise. + #' @param asPOSIX logical. If `TRUE`, for "standard", "gregorian" and + #' "proleptic_gregorian" calendars the output is a vector of `POSIXct` - + #' for other calendars an error will be thrown. Default value is `FALSE`. + #' + #' @return A character vector where each element represents a moment in + #' time according to the `format` specifier. + as_timestamp = function(format = NULL, asPOSIX = FALSE) { + if (asPOSIX && !self$cal$POSIX_compatible(self$offsets)) + stop("Cannot make a POSIX timestamp with this calendar.", call. = FALSE) + if (length(self$offsets) == 0L) return() + + time <- self$cal$offsets2time(self$offsets) + + if (is.null(format)) + format <- ifelse(self$cal$unit < 4L || .has_time(time), "timestamp", "date") + else if (!(format %in% c("date", "timestamp"))) + stop("Format specifier not recognized", call. = FALSE) + + if (asPOSIX) { + if (format == "date") ISOdate(time$year, time$month, time$day, 0L) + else ISOdatetime(time$year, time$month, time$day, time$hour, time$minute, time$second, "UTC") + } else .format_format(time, self$cal$timezone, format) + }, + + #' @description Format timestamps using a specific format string, using the + #' specifiers defined for the [base::strptime()] function, with + #' limitations. The only supported specifiers are `bBdeFhHImMpRSTYz%`. + #' Modifiers `E` and `O` are silently ignored. Other specifiers, including + #' their percent sign, are copied to the output as if they were adorning + #' text. + #' + #' The formatting is largely oblivious to locale. The reason for this is + #' that certain dates in certain calendars are not POSIX-compliant and the + #' system functions necessary for locale information thus do not work + #' consistently. The main exception to this is the (abbreviated) names of + #' months (`bB`), which could be useful for pretty printing in the local + #' language. For separators and other locale-specific adornments, use + #' local knowledge instead of depending on system locale settings; e.g. + #' specify `%m/%d/%Y` instead of `%D`. + #' + #' Week information, including weekday names, is not supported at all as a + #' "week" is not defined for non-standard CF calendars and not generally + #' useful for climate projection data. If you are working with observed + #' data and want to get pretty week formats, use the [as_timestamp()] + #' method to generate `POSIXct` timestamps (observed data generally uses a + #' "standard" calendar) and then use the [base::format()] function which + #' supports the full set of specifiers. + #' + #' @param format A character string with `strptime` format specifiers. If + #' omitted, the most economical format will be used: a full timestamp when + #' time information is available, a date otherwise. + #' + #' @return A vector of character strings with a properly formatted + #' timestamp. Any format specifiers not recognized or supported will be + #' returned verbatim. + format = function(format) { + if (length(self$offsets) == 0L) return(character(0L)) + + if (!requireNamespace("stringr", quietly = TRUE)) + stop("package `stringr` is required - please install it first", call. = FALSE) # nocov + + if (missing(format)) format <- "" + else if (!is.character(format) || length(format) != 1L) + stop("`format` argument must be a character string with formatting specifiers", call. = FALSE) + + ts <- self$cal$offsets2time(self$offsets) + .format_format(ts, self$cal$timezone, format) + }, + + #' @description Find the index in the time series for each timestamp given + #' in argument `x`. Values of `x` that are before the earliest value in + #' the time series will be returned as `0`; values of `x` that are after + #' the latest values in the time series will be returned as + #' `.Machine$integer.max`. Alternatively, when `x` is a numeric vector of + #' index values, return the valid indices of the same vector, with the + #' side effect being the attribute "CFTime" associated with the result. + #' + #' Matching also returns index values for timestamps that fall between two + #' elements of the time series - this can lead to surprising results when + #' time series elements are positioned in the middle of an interval (as + #' the CF Metadata Conventions instruct us to "reasonably assume"): a time + #' series of days in January would be encoded in a netCDF file as + #' `c("2024-01-01 12:00:00", "2024-01-02 12:00:00", "2024-01-03 12:00:00", ...)` + #' so `x <- c("2024-01-01", "2024-01-02", "2024-01-03")` would + #' result in `(NA, 1, 2)` (or `(NA, 1.5, 2.5)` with `method = "linear"`) + #' because the date values in `x` are at midnight. This situation is + #' easily avoided by ensuring that this `CFTime` instance has bounds set + #' (use `bounds(y) <- TRUE` as a proximate solution if bounds are not + #' stored in the netCDF file). See the Examples. + #' + #' If bounds are set, the indices are taken from those bounds. Returned + #' indices may fall in between bounds if the latter are not contiguous, + #' with the exception of the extreme values in `x`. + #' + #' Values of `x` that are not valid timestamps according to the calendar + #' of this `CFTime` instance will be returned as `NA`. + #' + #' `x` can also be a numeric vector of index values, in which case the + #' valid values in `x` are returned. If negative values are passed, the + #' positive counterparts will be excluded and then the remainder returned. + #' Positive and negative values may not be mixed. Using a numeric vector + #' has the side effect that the result has the attribute "CFTime" + #' describing the temporal dimension of the slice. If index values outside + #' of the range of `self` are provided, an error will be thrown. + #' + #' @param x Vector of character, POSIXt or Date values to find indices for, + #' or a numeric vector. + #' @param method Single value of "constant" or "linear". If `"constant"` or + #' when bounds are set on `self`, return the index value for each + #' match. If `"linear"`, return the index value with any fractional value. + #' + #' @return A numeric vector giving indices into the "time" dimension of the + #' dataset associated with `self` for the values of `x`. If there is at + #' least 1 valid index, then attribute "CFTime" contains an instance of + #' `CFTime` that describes the dimension of filtering the dataset + #' associated with `self` with the result of this function, excluding any + #' `NA`, `0` and `.Machine$integer.max` values. + indexOf = function(x, method = "constant") { + stopifnot(inherits(x, c("character", "POSIXt", "Date")) || is.numeric(x), + method %in% c("constant", "linear")) + + if (is.numeric(x)) { + if (!(all(x < 0, na.rm = TRUE) || all(x > 0, na.rm = TRUE))) + stop("Cannot mix positive and negative index values", call. = FALSE) + + intv <- (1:length(self$offsets))[x] + xoff <- self$offsets[x] + } else { + if (self$cal$unit > 4L) + stop("Parsing of timestamps on a 'month' or 'year' time unit is not supported.", call. = FALSE) + + xoff <- self$cal$parse(as.character(x))$offset + vals <- self$get_bounds() + vals <- if (is.null(vals)) self$offsets + else c(vals[1L, 1L], vals[2L, ]) + intv <- stats::approx(vals, 1L:length(vals), xoff, method = method, + yleft = 0, yright = .Machine$integer.max)$y + intv[which(intv == length(vals))] <- .Machine$integer.max } - } - return(c(y, m, d)) - }, yr, origin[2L], x) - data.frame(year = ymd[1L,], month = ymd[2L,], day = ymd[3L,], row.names = NULL) -} - -#' Julian calendar offsetting -#' -#' This is an internal function that should not be used outside of the CFtime package. -#' -#' @param x numeric. Vector of days to add to the origin. -#' @param origin numeric. Vector of year, month, day and seconds to add days to. -#' -#' @returns A data frame with time elements year, month and day in columns and as -#' many rows as the length of vector `x`. -#' @noRd -.offset2date_julian <- function(x, origin) { - common_days <- c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - leap_days <- c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - - # Is the leap day to consider ahead in the year from the base date (offset = 0) or in the next year (offset = 1) - offset <- as.integer(origin[2L] > 2L) - - # First process 4-year cycles of 1,461 days over the vector - yr <- origin[1L] + (x %/% 1461L) * 4L - x <- x %% 1461L - - # Remaining portion per datum - x <- x + origin[3L] - ymd <- mapply(function(y, m, d) { - repeat { - leap <- (y + offset) %% 4L == 0L - ydays <- 365L + as.integer(leap) - - if (d > ydays) { - d <- d - ydays - y <- y + 1L - } else break - } - - if (leap) month <- leap_days else month <- common_days - while (d > month[m]) { - d <- d - month[m] - m <- m + 1L - if (m == 13L) { - y <- y + 1L - m <- 1L + valid <- which(!is.na(intv) & intv > 0 & intv < .Machine$integer.max) + if (any(valid)) { + t <- CFTime$new(self$cal$definition, self$cal$name, xoff[valid]) + bnds <- self$get_bounds() + if (!is.null(bnds)) + t$set_bounds(bnds[, intv[valid], drop = FALSE]) + attr(intv, "CFTime") <- t + } + intv + }, + + #' @description Return bounds. + #' + #' @param format A string specifying a format for output, optional. + #' @return An array with dims(2, length(offsets)) with values for the + #' bounds. `NULL` if the bounds have not been set. + get_bounds = function(format) { + len <- length(self$offsets) + if (len == 0L) return(NULL) + + bnds <- self$bounds + if (is.logical(bnds)) { + if (!bnds) return(NULL) + + b <- seq(from = self$offsets[1L] - self$resolution * 0.5, + by = self$resolution, + length.out = len + 1L) + if (!missing(format)) { + ts <- self$cal$offsets2time(b) + b <- .format_format(ts, self$cal$timezone, format) + } + return(rbind(b[1L:len], b[2L:(len+1L)])) } - } - return(c(y, m, d)) - }, yr, origin[2L], x) - data.frame(year = ymd[1L,], month = ymd[2L,], day = ymd[3L,], row.names = NULL) -} -#' Standard calendar offsetting -#' -#' This is an internal function that should not be used outside of the CFtime package. -#' -#' @param x numeric. Vector of days to add to the origin. -#' @param origin numeric. Vector of year, month, day and seconds to add days to. -#' -#' @returns A data frame with time elements year, month and day in columns and as -#' many rows as the length of vector `x`. -#' @noRd -.offset2date_standard <- function(x, origin) { - common_days <- c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - leap_days <- c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - - # Is the leap day to consider ahead in the year from the base date (offset = 0) or in the next year (offset = 1) - offset <- as.integer(origin[2L] > 2L) - - x <- x + origin[3L] - ymd <- mapply(function(y, m, d) { - repeat { - test <- y + offset - leap <- (test %% 4L == 0L && test %% 100L > 0L) || test %% 400L == 0L - ydays <- 365L + as.integer(leap) - - if (d > ydays) { - d <- d - ydays - y <- y + 1L - } else break - } + # bnds is a matrix + if (missing(format)) return(bnds) + + ts <- self$cal$offsets2time(as.vector(bnds)) + b <- .format_format(ts, self$cal$timezone, format) + dim(b) <- c(2L, len) + b + }, + + #' @description Set the bounds of the `CFTime` instance. + #' + #' @param value The bounds to set, in units of the offsets. Either a matrix + #' `(2, length(self$offsets))` or a logical. + #' @return `self` invisibly. + set_bounds = function(value) { + if (isFALSE(value)) self$bounds <- FALSE + else if (isTRUE(value)) self$bounds <- TRUE + else { + off <- self$offsets + len <- length(off) + + if (len == 0L) + stop("Cannot set bounds when there are no offsets", call. = FALSE) + + if (is.matrix(value) && is.numeric(value)) { + if (!all(dim(value) == c(2L, len))) + stop("Replacement value has incorrect dimensions", call. = FALSE) + } else stop("Replacement value must be a numeric matrix or a single logical value", call. = FALSE) + + if (!(all(value[2L,] >= off) && all(off >= value[1L,]))) + stop("Values of the replacement value must surround the offset values", call. = FALSE) + + # Compress array to `TRUE`, if regular + if (len > 1L && identical(value[1L,2L:len], value[2L,1L:(len-1L)]) && + diff(range(diff(value[1L,]))) == 0) value <- TRUE + + self$bounds <- value + invisible(self) + } + }, + + #' This method returns `TRUE` if the time series has uniformly distributed + #' time steps between the extreme values, `FALSE` otherwise. First test + #' without sorting; this should work for most data sets. If not, only then + #' offsets are sorted. For most data sets that will work but for implied + #' resolutions of month, season, year, etc based on a "days" or finer + #' calendar unit this will fail due to the fact that those coarser units + #' have a variable number of days per time step, in all calendars except for + #' `360_day`. For now, an approximate solution is used that should work in + #' all but the most non-conformal exotic arrangements. + #' + #' @return `TRUE` if all time steps are equidistant, `FALSE` otherwise, or + #' `NA` if no offsets have been set. + equidistant = function() { + if (length(self$offsets) == 0L) return(NA) + out <- all(diff(self$offsets) == self$resolution) + if (!out) { + doff <- diff(sort(self$offsets)) + out <- all(doff == self$resolution) + if (!out) { + # Don't try to make sense of totally non-standard arrangements such as + # calendar units "years" or "months" describing sub-daily time steps. + # Also, 360_day calendar should be well-behaved so we don't want to get here. + if (self$cal$unit > 4L || inherits(self$cal, "CFCalendar360")) return(FALSE) + + # Check if we have monthly or yearly data on a finer-scale calendar + # This is all rather approximate but should be fine in most cases + # This accommodates middle-of-the-time-period offsets as per the + # CF Metadata Conventions + # Please report problems at https://github.com/pvanlaake/CFtime/issues + ddays <- range(doff) * CFt$units$per_day[self$cal$unit] + return((ddays[1] >= 28 && ddays[2] <= 31) || # months + (ddays[1] >= 8 && ddays[2] <= 11) || # dekads + (ddays[1] >= 90 && ddays[2] <= 92) || # seasons, quarters + (ddays[1] >= 365 && ddays[2] <= 366)) # years + } + } + out + }, + + #' @description Given two extreme character timestamps, return a logical vector of a length + #' equal to the number of time steps in the time series with values `TRUE` + #' for those time steps that fall between the two extreme values, `FALSE` + #' otherwise. + #' + #' **NOTE** Giving crap as the earlier timestamp will set that value to 0. So + #' invalid input will still generate a result. To be addressed. Crap in later + #' timestamp is not tolerated. + #' + #' @param extremes Character vector of two timestamps that represent the + #' extremes of the time period of interest. + #' @param closed Is the right side closed, i.e. included in the result? + #' @return A logical vector with a length equal to the number of time steps in + #' `self` with values `TRUE` for those time steps that fall between the two + #' extreme values, `FALSE` otherwise. The earlier timestamp is included, the + #' later timestamp is excluded. A specification of `c("2022-01-01", "2023-01-01)` + #' will thus include all time steps that fall in the year 2022. + #' + #' An attribute 'CFTime' will have the same definition as `self` but with offsets + #' corresponding to the time steps falling between the two extremes. If there + #' are no values between the extremes, the attribute is `NULL`. + slab = function(extremes, closed) { + if (!is.character(extremes) || length(extremes) != 2L) + stop("Second argument must be a character vector of two timestamps", call. = FALSE) + if (extremes[2L] < extremes[1L]) extremes <- c(extremes[2L], extremes[1L]) + + ext <- self$cal$parse(extremes)$offset + if (is.na(ext[1L])) ext[1L] <- 0 + off <- self$offsets + if (ext[1L] > max(off) || is.na(ext[2L])) { + out <- rep(FALSE, length(off)) + attr(out, "CFTime") <- NULL + } else { + out <- if (closed) off >= ext[1L] & off <= ext[2L] + else off >= ext[1L] & off < ext[2L] + t <- CFTime$new(self$cal$definition, self$cal$name, off[out]) + bnds <- self$get_bounds() + if (!is.null(bnds)) + t$set_bounds(bnds[, out]) + attr(out, "CFTime") <- t + } + out + }, + + #' @description Can the time series be converted to POSIXt? + #' @return `TRUE` if the calendar support coversion to POSIXt, `FALSE` + #' otherwise. + POSIX_compatible = function() { + self$cal$POSIX_compatible(self$offsets) + }, + + #' @description Create a factor for a `CFTime` instance. + #' + #' When argument `breaks` is one of `"year", "season", "quarter", "month", + #' "dekad", "day"`, a factor is generated like by [CFfactor()]. When + #' `breaks` is a vector of character timestamps a factor is produced with + #' a level for every interval between timestamps. The last timestamp, + #' therefore, is only used to close the interval started by the + #' pen-ultimate timestamp - use a distant timestamp (e.g. `range(x)[2]`) + #' to ensure that all offsets to the end of the CFTime time series are + #' included, if so desired. The last timestamp will become the upper bound + #' in the `CFTime` instance that is returned as an attribute to this + #' function so a sensible value for the last timestamp is advisable. + #' + #' This method works similar to [base::cut.POSIXt()] but there are some + #' differences in the arguments: for `breaks` the set of options is + #' different and no preceding integer is allowed, `labels` are always + #' assigned using values of `breaks`, and the interval is always + #' left-closed. + #' + #' @param breaks A character string of a factor period (see [CFfactor()] for + #' a description), or a character vector of timestamps that conform to the + #' calendar of `x`, with a length of at least 2. Timestamps must be given + #' in ISO8601 format, e.g. "2024-04-10 21:31:43". + #' + #' @return A factor with levels according to the `breaks` argument, with + #' attributes 'period', 'era' and 'CFTime'. When `breaks` is a factor + #' period, attribute 'period' has that value, otherwise it is '"day"'. + #' When `breaks` is a character vector of timestamps, attribute 'CFTime' + #' holds an instance of `CFTime` that has the same definition as `x`, but + #' with (ordered) offsets generated from the `breaks`. Attribute 'era' + #' is always -1. + cut = function(breaks) { + if (missing(breaks) || !is.character(breaks) || (len <- length(breaks)) < 1L) + stop("Argument 'breaks' must be a character vector with at least 1 value", call. = FALSE) + + if(len == 1L) { + breaks <- sub("s$", "", tolower(breaks)) + if (breaks %in% CFt$factor_periods) + return(CFfactor(self, breaks)) # FIXME after CFfactor is done + else stop("Invalid specification of 'breaks'", call. = FALSE) + } - if (leap) month <- leap_days else month <- common_days + # breaks is a character vector of multiple timestamps + if (self$cal$unit > 4L) stop("Factorizing on a 'month' or 'year' time unit is not supported", call. = FALSE) + time <- self$cal$parse(breaks) + if (anyNA(time$year)) + stop("Invalid specification of 'breaks'", call. = FALSE) + sorted <- order(time$offset) + ooff <- time$offset[sorted] + intv <- findInterval(self$offsets, ooff) + intv[which(intv %in% c(0L, len))] <- NA + f <- factor(intv, labels = breaks[sorted][1L:(len-1L)]) + + # Attributes + bnds <- rbind(ooff[1L:(len-1L)], ooff[2L:len]) + off <- bnds[1L, ] + (bnds[2L, ] - bnds[1L, ]) * 0.5 + t <- CFTime$new(self$cal$definition, self$cal$name, off) + bounds(t) <- bnds + attr(f, "period") <- "day" + attr(f, "era") <- -1L + attr(f, "CFTime") <- t + f + }, + + #' @description Generate a factor for the offsets, or a part thereof. This is + #' specifically interesting for creating factors from the date part of the + #' time series that aggregate the time series into longer time periods (such + #' as month) that can then be used to process daily CF data sets using, for + #' instance, `tapply()`. + #' + #' The factor will respect the calendar that the time series is built on. + #' + #' The factor will be generated in the order of the offsets. While typical + #' CF-compliant data sources use ordered time series there is, however, no + #' guarantee that the factor is ordered. For most processing with a factor + #' the ordering is of no concern. + #' + #' If the `era` parameter is specified, either as a vector of years to + #' include in the factor, or as a list of such vectors, the factor will only + #' consider those values in the time series that fall within the list of + #' years, inclusive of boundary values. Other values in the factor will be + #' set to `NA`. The years need not be contiguous, within a single vector or + #' among the list items, or in order. + #' + #' The following periods are supported by this method: + #' + #' \itemize{ + #' \item `year`, the year of each offset is returned as "YYYY". + #' \item `season`, the meteorological season of each offset is returned as + #' "Sx", with x being 1-4, preceeded by "YYYY" if no `era` is + #' specified. Note that December dates are labeled as belonging to the + #' subsequent year, so the date "2020-12-01" yields "2021S1". This implies + #' that for standard CMIP files having one or more full years of data the + #' first season will have data for the first two months (January and + #' February), while the final season will have only a single month of data + #' (December). + #' \item `quarter`, the calendar quarter of each offset is returned as "Qx", + #' with x being 1-4, preceeded by "YYYY" if no `era` is specified. + #' \item `month`, the month of each offset is returned as "01" to + #' "12", preceeded by "YYYY-" if no `era` is specified. This is the default + #' period. + #' \item `dekad`, ten-day periods are returned as + #' "Dxx", where xx runs from "01" to "36", preceeded by "YYYY" if no `era` + #' is specified. Each month is subdivided in dekads as follows: 1- days 01 - + #' 10; 2- days 11 - 20; 3- remainder of the month. + #' \item `day`, the month and day of each offset are returned as "MM-DD", + #' preceeded by "YYYY-" if no `era` is specified. + #' } + #' + #' It is not possible to create a factor for a period that is shorter than + #' the temporal resolution of the calendar. As an example, if the calendar + #' has a monthly unit, a dekad or day factor cannot be created. + #' + #' Creating factors for other periods is not supported by this method. + #' Factors based on the timestamp information and not dependent on the + #' calendar can trivially be constructed from the output of the + #' [as_timestamp()] function. + #' + #' For non-era factors the attribute 'CFTime' of the result contains a + #' `CFTime` instance that is valid for the result of applying the factor to + #' a resource that this instance is associated with. In other words, if + #' `CFTime` instance 'At' describes the temporal dimension of resource 'A' + #' and a factor 'Af' is generated from `Af <- At$factor()`, then + #' `Bt <- attr(Af, "CFTime")` describes the temporal dimension of the result + #' of, say, `B <- apply(A, 1:2, tapply, Af, FUN)`. The 'CFTime' attribute is + #' `NULL` for era factors. + #' + #' @param period character. A character string with one of the values + #' "year", "season", "quarter", "month" (the default), "dekad" or "day". + #' @param era numeric or list, optional. Vector of years for which to + #' construct the factor, or a list whose elements are each a vector of + #' years. If `era` is not specified, the factor will use the entire time + #' series for the factor. + #' @return If `era` is a single vector or not specified, a factor with a + #' length equal to the number of offsets in this instance. If `era` is a + #' list, a list with the same number of elements and names as `era`, + #' each containing a factor. Elements in the factor will be set to `NA` + #' for time series values outside of the range of specified years. + #' + #' The factor, or factors in the list, have attributes 'period', 'era' + #' and 'CFTime'. Attribute 'period' holds the value of the `period` + #' argument. Attribute 'era' indicates the number of years that are + #' included in the era, or -1 if no `era` is provided. Attribute + #' 'CFTime' holds an instance of `CFTime` that has the same definition as + #' this instance, but with offsets corresponding to the mid-point of + #' non-era factor levels; if the `era` argument is specified, + #' attribute 'CFTime' is `NULL`. + factor = function(period = "month", era = NULL) { + if (length(self$offsets) < 10L) stop("Cannot create a factor for very short time series", call. = FALSE) + + period <- tolower(period) + if (!((length(period) == 1L) && (period %in% CFt$factor_periods))) + stop("Period specifier must be a single value of a supported period", call. = FALSE) + + # No fine-grained period factors for coarse source data + timestep <- CFt$units$seconds[self$cal$unit] * self$resolution; + if ((period == "year") && (timestep > 86400 * 366) || + (period %in% c("season", "quarter")) && (timestep > 86400 * 90) || # Somewhat arbitrary + (period == "month") && (timestep > 86400 * 31) || + (period == "dekad") && (timestep > 86400) || # Must be constructed from daily or finer data + (period == "day") && (timestep > 86400)) # Must be no longer than a day + stop("Cannot produce a short period factor from source data with long time interval", call. = FALSE) + + time <- self$cal$offsets2time(self$offsets) + months <- c("01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12") + + if (is.null(era)) { + # Create the factor for the specified period as well as bounds dates for a + # new CFtime instance for the factor. Lower bounds for the factor level is + # easy, upper bound of last level takes effort. + switch(period, + "year" = { + out <- as.factor(sprintf("%04d", time$year)) + l <- levels(out) + dt <- c(paste0(l, "-01-01"), sprintf("%04d-01-01", as.integer(l[nlevels(out)]) + 1L)) + }, + "season" = { + if (!requireNamespace("stringr")) + stop("Must install package `stringr` to use this functionality.", call. = FALSE) + + out <- as.factor( + ifelse(time$month == 12L, sprintf("%04dS1", time$year + 1L), + sprintf("%04dS%d", time$year, time$month %/% 3L + 1L))) + l <- levels(out) + dt <- ifelse(substr(l, 6L, 6L) == "1", paste0(as.integer(substr(l, 1L, 4L)) - 1L, "-12-01"), + stringr::str_replace_all(l, c("S2" = "-03-01", "S3" = "-06-01", "S4" = "-09-01"))) + ll <- l[nlevels(out)] + lp <- as.integer(substr(ll, 6L, 6L)) + if (lp == 1L) + dt <- c(dt, sprintf("%04d-03-01", as.integer(substr(ll, 1L, 4L)) + 1L)) + else dt <- c(dt, sprintf("%s-%02d-01", substr(ll, 1L, 4L), lp * 3L)) + }, + "quarter" = { + if (!requireNamespace("stringr")) + stop("Must install package `stringr` to use this functionality.", call. = FALSE) + + out <- as.factor(sprintf("%04dQ%d", time$year, (time$month - 1L) %/% 3L + 1L)) + l <- levels(out) + dt <- stringr::str_replace_all(l, c("Q1" = "-01-01", "Q2" = "-04-01", "Q3" = "-07-01", "Q4" = "-10-01")) + ll <- l[nlevels(out)] + lp <- as.integer(substr(ll, 6L, 6L)) + if (lp == 4L) + dt <- c(dt, sprintf("%04d-01-01", as.integer(substr(ll, 1L, 4L)) + 1L)) + else dt <- c(dt, sprintf("%s-%02d-01", substr(ll, 1L, 4L), lp * 3L + 1L)) + }, + "month" = { + out <- as.factor(sprintf("%04d-%s", time$year, months[time$month])) + l <- levels(out) + dt <- paste0(l, "-01") + ll <- l[nlevels(out)] + lp <- as.integer(substr(ll, 6L, 7L)) + if (lp == 12L) + dt <- c(dt, sprintf("%04d-01-01", as.integer(substr(ll, 1L, 4L)) + 1L)) + else dt <- c(dt, sprintf("%s-%02d-01", substr(ll, 1L, 4L), lp + 1L)) + }, + "dekad" = { + out <- as.factor(sprintf("%04dD%02d", time$year, (time$month - 1L) * 3L + pmin.int((time$day - 1L) %/% 10L + 1L, 3L))) + l <- levels(out) + dk <- as.integer(substr(l, 6L, 7L)) - 1L + dt <- sprintf("%s-%02d-%s", substr(l, 1L, 4L), dk %/% 3L + 1L, c("01", "11", "21")[dk %% 3L + 1L]) + ll <- l[nlevels(out)] + lp <- as.integer(substr(ll, 6L, 7L)) + yr <- as.integer(substr(ll, 1L, 4L)) + if (lp == 36L) + dt <- c(dt, sprintf("%04d-01-01", yr + 1L)) + else dt <- c(dt, sprintf("%04d-%02d-%s", yr, (lp + 1L) %/% 3L + 1L, c("01", "11", "21")[(lp + 1L) %% 3L + 1L])) + }, + "day" = { + out <- as.factor(sprintf("%04d-%02d-%02d", time$year, time$month, time$day)) + l <- levels(out) + lp <- l[nlevels(out)] + last <- self$cal$offsets2time(self$cal$parse(lp)$offset) + dt <- c(l, sprintf("%04d-%02d-%02d", last$year, last$month, last$day)) + } + ) + + # Convert bounds dates to an array of offsets, find mid-points, create new CFTime instance + off <- self$cal$parse(dt)$offset + off[is.na(off)] <- 0 # This can happen only when the time series starts at or close to the origin, for seasons + noff <- length(off) + bnds <- rbind(off[1L:(noff - 1L)], off[2L:noff]) + off <- bnds[1L,] + (bnds[2L,] - bnds[1L,]) * 0.5 + new_cf <- CFTime$new(self$cal$definition, self$cal$name, off) + bounds(new_cf) <- TRUE + + # Bind attributes to the factor + attr(out, "era") <- -1L + attr(out, "period") <- period + attr(out, "CFTime") <- new_cf + return(out) + } - while (d > month[m]) { - d <- d - month[m] - m <- m + 1L - if (m == 13L) { - y <- y + 1L - m <- 1L + # Era factor + if (is.numeric(era)) ep <- list(era) + else if ((is.list(era) && all(unlist(lapply(era, is.numeric))))) ep <- era + else stop("When specified, the `era` parameter must be a numeric vector or a list thereof", call. = FALSE) + + out <- lapply(ep, function(years) { + f <- switch(period, + "year" = ifelse(time$year %in% years, sprintf("%04d", time$year), NA_character_), + "season" = ifelse((time$month == 12L) & ((time$year + 1L) %in% years), "S1", + ifelse((time$month < 12L) & (time$year %in% years), sprintf("S%d", time$month %/% 3L + 1L), NA_character_)), + "quarter" = ifelse(time$year %in% years, sprintf("Q%d", (time$month - 1L) %/% 3L + 1L), NA_character_), + "month" = ifelse(time$year %in% years, months[time$month], NA_character_), + "dekad" = ifelse(time$year %in% years, sprintf("D%02d", (time$month - 1L) * 3L + pmin.int((time$day - 1L) %/% 10L + 1L, 3L)), NA_character_), + "day" = ifelse(time$year %in% years, sprintf("%s-%02d", months[time$month], time$day), NA_character_) + ) + f <- as.factor(f) + attr(f, "era") <- length(years) + attr(f, "period") <- period + attr(f, "CFTime") <- NULL + f + }) + if (is.numeric(era)) out <- out[[1L]] + else names(out) <- names(era) + out + }, + + #' @description Given a factor as produced by `CFTime$factor()`, this method + #' will return a numeric vector with the number of time units in each + #' level of the factor. + #' + #' The result of this method is useful to convert between absolute and + #' relative values. Climate change anomalies, for instance, are usually + #' computed by differencing average values between a future period and a + #' baseline period. Going from average values back to absolute values for + #' an aggregate period (which is typical for temperature and + #' precipitation, among other variables) is easily done with the result of + #' this method, without having to consider the specifics of the calendar + #' of the data set. + #' + #' If the factor `f` is for an era (e.g. spanning multiple years and the + #' levels do not indicate the specific year), then the result will + #' indicate the number of time units of the period in a regular single + #' year. In other words, for an era of 2041-2060 and a monthly factor on a + #' standard calendar with a `days` unit, the result will be + #' `c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)`. Leap days are thus + #' only considered for the `366_day` and `all_leap` calendars. + #' + #' Note that this function gives the number of time units in each level of + #' the factor - the actual number of data points in the time series per + #' factor level may be different. Use [CFfactor_coverage()] to determine + #' the actual number of data points or the coverage of data points + #' relative to the factor level. + #' + #' @param f A factor or a list of factors derived from the method + #' `CFTime$factor()`. + #' @return If `f` is a factor, a numeric vector with a length equal to the + #' number of levels in the factor, indicating the number of time units in + #' each level of the factor. If `f` is a list of factors, a list with each + #' element a numeric vector as above. + factor_units = function(f) { + if (is.list(f)) factors <- f else factors <- list(f) + if (!(all(unlist(lapply(factors, function(x) is.factor(x) && is.numeric(attr(x, "era")) && + attr(x, "period") %in% CFt$factor_periods))))) + stop("Argument `f` must be a factor generated by the method `CFTime$factor()`", call. = FALSE) + + out <- lapply(factors, function(fac) .factor_units(fac, self$cal, CFt$units$per_day[self$cal$unit])) + if (is.factor(f)) out <- out[[1L]] + out + }, + + #' @description Calculate the number of time elements, or the relative + #' coverage, in each level of a factor generated by `CFTime$factor()`. + #' + #' @param f A factor or a list of factors derived from the method + #' `CFTime$factor()`. + #' @param coverage "absolute" or "relative". + #' @return If `f` is a factor, a numeric vector with a length equal to the + #' number of levels in the factor, indicating the number of units from the + #' time series contained in each level of the factor when + #' `coverage = "absolute"` or the proportion of units present relative to the + #' maximum number when `coverage = "relative"`. If `f` is a list of factors, a + #' list with each element a numeric vector as above. + factor_coverage = function(f, coverage = "absolute") { + if (is.list(f)) factors <- f else factors <- list(f) + if (!(all(unlist(lapply(factors, function(x) is.factor(x) && is.numeric(attr(x, "era")) && + attr(x, "period") %in% CFt$factor_periods))))) + stop("Argument `f` must be a factor generated by the method `CFTime$factor()`", call. = FALSE) + + if (!(is.character(coverage) && coverage %in% c("absolute", "relative"))) + stop("Argument `coverage` must be a character string with a value of 'absolute' or 'relative'", call. = FALSE) + + if (coverage == "relative") { + out <- lapply(factors, function(fac) { + res <- tabulate(fac) / .factor_units(fac, self$cal, CFt$units$per_day[self$cal$unit]) + yrs <- attr(fac, "era") + if (yrs > 0) res <- res / yrs + return(res) + }) + } else { + out <- lapply(factors, tabulate) } - } - return(c(y, m, d)) - }, origin[1L], origin[2L], x) - data.frame(year = ymd[1L,], month = ymd[2L,], day = ymd[3L,], row.names = NULL) -} + if (is.factor(f)) out <- out[[1L]] + out + } + ) +) diff --git a/R/CFutils.R b/R/CFutils.R deleted file mode 100644 index b04ee46..0000000 --- a/R/CFutils.R +++ /dev/null @@ -1,132 +0,0 @@ -#' Return the number of days in a month given a certain CF calendar -#' -#' Given a vector of dates as strings in ISO 8601 or UDUNITS format and a `CFtime` object, -#' this function will return a vector of the same length as the dates, -#' indicating the number of days in the month according to the calendar -#' specification. If no vector of days is supplied, the function will return an -#' integer vector of length 12 with the number of days for each month of the -#' calendar (disregarding the leap day for `standard` and `julian` calendars). -#' -#' @param cf CFtime. The CFtime definition to use. -#' @param x character. An optional vector of dates as strings with format -#' `YYYY-MM-DD`. Any time part will be silently ingested. -#' -#' @returns A vector indicating the number of days in each month for the vector -#' of dates supplied as a parameter to the function. If no dates are supplied, -#' the number of days per month for the calendar as a vector of length 12. -#' Invalidly specified dates will result in an `NA` value. -#' @export -#' @seealso When working with factors generated by [CFfactor()], it is usually -#' better to use [CFfactor_units()] as that will consider leap days for -#' non-epoch factors. [CFfactor_units()] can also work with other time periods -#' and datum units, such as "hours per month", or "days per season". -#' @examples -#' dates <- c("2021-11-27", "2021-12-10", "2022-01-14", "2022-02-18") -#' cf <- CFtime("days since 1850-01-01", "standard") -#' month_days(cf, dates) -#' -#' cf <- CFtime("days since 1850-01-01", "360_day") -#' month_days(cf, dates) -#' -#' cf <- CFtime("days since 1850-01-01", "all_leap") -#' month_days(cf, dates) -#' -#' month_days(cf) -month_days <- function(cf, x = NULL) { - stopifnot(methods::is(cf, "CFtime")) - cal_id <- cf@datum@cal_id - - days_in_month <- c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - leapdays_in_month <- c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - - # No dates supplied: return standard number of days per month - if (is.null(x)) { - if (cal_id %in% c(1L, 2L, 4L)) return(days_in_month) - if (cal_id == 3L) return(rep(30L, 12L)) - return(leapdays_in_month) - } - - # Argument x supplied - if (!(is.character(x))) stop("Argument `x` must be a character vector of dates in 'YYYY-MM-DD' format") - - ymd <- .parse_timestamp(cf@datum, x) - if (anyNA(ymd$year)) warning("Some dates could not be parsed. Result contains `NA` values.") - - if (cal_id == 3L) { # 360_day - res <- rep(30L, length(x)) - res[which(is.na(ymd$year))] <- NA - return(res) - } - - if (cal_id == 4L) return(days_in_month[ymd$month]) - if (cal_id == 5L) return(leapdays_in_month[ymd$month]) - - # Standard and julian calendars - ifelse(.is_leap_year(ymd$year, cal_id), leapdays_in_month[ymd$month], days_in_month[ymd$month]) -} - -#' Check if the supplied year, month and day form a valid date in the specified -#' calendar. -#' -#' This is an internal function that should not be used outside of the CFtime package. -#' -#' @param yr numeric. The year to test, must be in range 1:9999. -#' @param mon numeric. The month to test, must be in range 1:12 -#' @param day numeric. The day to test, must be in the range permitted by the calendar. -#' @param cal_id numeric. Identifier of the calendar to use to test the validity of the date. -#' -#' @returns boolean. TRUE if the date is valid, FALSE otherwise. -#' @noRd -.is_valid_calendar_date <- function(yr, mon, day, cal_id) { - if (is.na(yr) || is.na(mon)) return(FALSE) - - # Check valid date ranges, no extended syntax - if ((yr < 1L) || (yr > 9999L)) return(FALSE) # year out of range - if ((mon < 1L) || (mon > 12L)) return(FALSE) # month out of range - if (is.na(day)) return(TRUE) # day not specified - if ((day >= 1L) && (day <= 28L)) return(TRUE) # day in safe range, 90% of valid cases - else if ((day < 1L) || day > 31L) return(FALSE) # day out of range - - # 360_day calendar: oddball case for month length - if (cal_id == 3L) return(day <= 30L) - - # Now all dates should be in regular-length months, but check for leap years - # Day is in range 29:31 because day in range 1:28 already passed - if (mon == 2L) { # February - if (day > 29L) return(FALSE) - if (cal_id == 5L) return(TRUE) # all_leap - if (cal_id == 4L) return(FALSE) # no_leap - if (cal_id == 2L) return(yr %% 4L == 0L) # julian: every 4th year is a leap year - return(((yr %% 4L == 0L) && (yr %% 100L > 0L)) || (yr %% 400L == 0L)) # standard calendar - } - return(!((mon %in% c(4L, 6L, 9L, 11L)) && (day == 31L))) # months other than February -} - -#' Flag which years are leap years, given a certain CF calendar -#' -#' This is an internal function that should not be used outside of the CFtime package. -#' -#' @param yr numeric. Vector of years to test. -#' @param cal integer. The id of the calendar. -#' -#' @returns A logical vector of the same length as argument `yr` which is `TRUE` -#' for elements that are leap years for the given calendar, `FALSE` otherwise. -#' @noRd -.is_leap_year <- function(yr, cal) { - switch (cal, - ((yr %% 4L == 0L) & (yr %% 100L > 0L)) | (yr %% 400L == 0L), - yr %% 4L == 0L, - rep(FALSE, length(yr)), - rep(FALSE, length(yr)), - rep(TRUE, length(yr))) -} - -#' Display the structure of a CFdatum instance -#' -#' @param object `CFdatum` instance to print structure of. -#' @param ... Ignored. -#' @returns Nothing. Prints information to the console. -#' @export -str.CFdatum <- function(object, ...) { - cat(" ", object@definition, " [ ", object@calendar, " calendar ]\n", sep = "") -} diff --git a/R/api.R b/R/api.R new file mode 100644 index 0000000..6eb0c11 --- /dev/null +++ b/R/api.R @@ -0,0 +1,795 @@ +#' Create a CFTime object +#' +#' This function creates an instance of the [CFTime] class. The arguments to the +#' call are typically read from a CF-compliant data file with climatological +#' observations or climate projections. Specification of arguments can also be +#' made manually in a variety of combinations. +#' +#' @param definition A character string describing the time coordinate. +#' @param calendar A character string describing the calendar to use with the +#' time dimension definition string. Default value is "standard". +#' @param offsets Numeric or character vector, optional. When numeric, a vector +#' of offsets from the origin in the time series. When a character vector of +#' length 2 or more, timestamps in ISO8601 or UDUNITS format. When a character +#' string, a timestamp in ISO8601 or UDUNITS format and then a time series +#' will be generated with a separation between steps equal to the unit of +#' measure in the definition, inclusive of the definition timestamp. The unit +#' of measure of the offsets is defined by the time series definition. +#' @returns An instance of the `CFTime` class. +#' @export +#' @name CFtime-function +#' @examples +#' CFtime("days since 1850-01-01", "julian", 0:364) +#' +#' CFtime("hours since 2023-01-01", "360_day", "2023-01-30T23:00") +CFtime <- function(definition, calendar = "standard", offsets = NULL) { + CFTime$new(definition, calendar, offsets) +} + +# ============================================================================== +# Functions to access CFTime properties and methods + +#' @aliases properties +#' @title Properties of a CFTime object +#' +#' @description These functions return the properties of an instance of the +#' [CFTime] class. The properties are all read-only, but offsets can be added +#' using the `+` operator. +#' +#' @param t An instance of `CFTime`. +#' +#' @returns `calendar()` and `unit()` return a character string. +#' `origin()` returns a data frame of timestamp elements with a single row +#' of data. `timezone()` returns the calendar time zone as a character +#' string. `offsets()` returns a vector of offsets or `NULL` if no offsets +#' have been set. +#' +#' @examples +#' t <- CFtime("days since 1850-01-01", "julian", 0:364) +#' definition(t) +#' calendar(t) +#' unit(t) +#' timezone(t) +#' origin(t) +#' offsets(t) +#' resolution(t) + +#' @describeIn properties The definition string of the `CFTime` instance. +#' @export +definition <- function(t) t$cal$definition + +#' @describeIn properties The calendar of the `CFTime` instance. +#' @export +calendar <- function(t) t$cal$name + +#' @describeIn properties The unit of the `CFTime` instance. +#' @export +unit <- function(t) CFt$units$name[t$cal$unit] + +#' @describeIn properties The origin of the `CFTime` instance in timestamp elements. +#' @export +origin <- function(t) t$cal$origin + +#' @describeIn properties The time zone of the calendar of the `CFTime` instance as a character string. +#' @export +timezone <- function(t) t$cal$timezone + +#' @describeIn properties The offsets of the `CFTime` instance as a numeric vector. +#' @export +offsets <- function(t) t$offsets + +#' @describeIn properties The average separation between the offsets in the `CFTime` instance. +#' @export +resolution <- function(t) t$resolution + +#' Bounds of the time offsets +#' +#' CF-compliant netCDF files store time information as a single offset value for +#' each step along the dimension, typically centered on the valid interval of +#' the data (e.g. 12-noon for day data). Optionally, the lower and upper values +#' of the valid interval are stored in a so-called "bounds" variable, as an +#' array with two rows (lower and higher value) and a column for each offset. +#' With function `bounds()<-` those bounds can be set for a `CFTime` instance. +#' The bounds can be retrieved with the `bounds()` function. +#' +#' @param x A `CFTime` instance. +#' @param format Optional. A single string with format specifiers, see +#' [CFtime::format()] for details. +#' @return If bounds have been set, an array of bounds values with dimensions +#' (2, length(offsets)). The first row gives the lower bound, the second row +#' the upper bound, with each column representing an offset of `x`. If the +#' `format` argument is specified, the bounds values are returned as strings +#' according to the format. `NULL` when no bounds have been set. +#' @aliases bounds +#' @export +#' @examples +#' t <- CFtime("days since 2024-01-01", "standard", seq(0.5, by = 1, length.out = 366)) +#' as_timestamp(t)[1:3] +#' bounds(t) <- rbind(0:365, 1:366) +#' bounds(t)[, 1:3] +#' bounds(t, "%d-%b-%Y")[, 1:3] +bounds <- function(x, format) { + x$get_bounds(format) +} + +#' @rdname bounds +#' @param value A `matrix` (or `array`) with dimensions (2, length(offsets)) +#' giving the lower (first row) and higher (second row) bounds of each offset +#' (this is the format that the CF Metadata Conventions uses for storage in +#' netCDF files). Use `FALSE` to unset any previously set bounds, `TRUE` to +#' set regular bounds at mid-points between the offsets (which must be regular +#' as well). +#' @export +`bounds<-` <- function(x, value) { + x$set_bounds(value) + x +} + +#' The length of the offsets contained in the `CFTime` instance. +#' +#' @param x The `CFTime` instance whose length will be returned +#' +#' @return The number of offsets in the specified `CFTime` instance. +#' @export +#' +#' @examples +#' t <- CFtime("days since 1850-01-01", "julian", 0:364) +#' length(t) +length.CFTime <- function(x) base::length(x$offsets) + +#' Return the timestamps contained in the `CFTime` instance. +#' +#' @param x The `CFTime` instance whose timestamps will be returned. +#' @param ... Ignored. +#' +#' @return The timestamps in the specified `CFTime` instance. +#' @export +#' +#' @examples +#' t <- CFtime("days since 1850-01-01", "julian", 0:364) +#' as.character(t) +as.character.CFTime <- function(x, ...) { + x$as_timestamp() +} + +#' Create a factor for a `CFTime` instance +#' +#' Method for [base::cut()] applied to [CFTime] objects. +#' +#' When `breaks` is one of `"year", "season", "quarter", "month", "dekad", +#' "day"` a factor is generated like by [CFfactor()]. +#' +#' When `breaks` is a vector of character timestamps a factor is produced with a +#' level for every interval between timestamps. The last timestamp, therefore, +#' is only used to close the interval started by the pen-ultimate timestamp - +#' use a distant timestamp (e.g. `range(x)[2]`) to ensure that all offsets to +#' the end of the CFTime time series are included, if so desired. The last +#' timestamp will become the upper bound in the `CFTime` instance that is +#' returned as an attribute to this function so a sensible value for the last +#' timestamp is advisable. +#' +#' This method works similar to [base::cut.POSIXt()] but there are some +#' differences in the arguments: for `breaks` the set of options is different +#' and no preceding integer is allowed, `labels` are always assigned using +#' values of `breaks`, and the interval is always left-closed. +#' +#' @param x An instance of `CFTime`. +#' @param breaks A character string of a factor period (see [CFfactor()] for a +#' description), or a character vector of timestamps that conform to the +#' calendar of `x`, with a length of at least 2. Timestamps must be given in +#' ISO8601 format, e.g. "2024-04-10 21:31:43". +#' @param ... Ignored. +#' @returns A factor with levels according to the `breaks` argument, with +#' attributes 'period', 'era' and 'CFTime'. When `breaks` is a factor +#' period, attribute 'period' has that value, otherwise it is '"day"'. When +#' `breaks` is a character vector of timestamps, attribute 'CFTime' holds an +#' instance of `CFTime` that has the same definition as `x`, but with (ordered) +#' offsets generated from the `breaks`. Attribute 'era' is always -1. +#' @aliases cut +#' @seealso [CFfactor()] produces a factor for several fixed periods, including +#' for eras. +#' @export +#' @examples +#' x <- CFtime("days since 2021-01-01", "365_day", 0:729) +#' breaks <- c("2022-02-01", "2021-12-01", "2023-01-01") +#' cut(x, breaks) +cut.CFTime <- function (x, breaks, ...) { + if (!inherits(x, "CFTime")) + stop("Argument 'x' must be a CFTime instance", call. = FALSE) + x$cut(breaks) +} + +#' Find the index of timestamps in the time series +#' +#' Find the index in the time series for each timestamp given in argument `x`. +#' Values of `x` that are before the earliest value in `y` will be returned as +#' `0`; values of `x` that are after the latest values in `y` will be returned +#' as `.Machine$integer.max`. Alternatively, when `x` is a numeric vector of +#' index values, return the valid indices of the same vector, with the side +#' effect being the attribute "CFTime" associated with the result. +#' +#' Timestamps can be provided as vectors of character strings, `POSIXct` or +#' `Date.` +#' +#' Matching also returns index values for timestamps that fall between two +#' elements of the time series - this can lead to surprising results when time +#' series elements are positioned in the middle of an interval (as the CF +#' Metadata Conventions instruct us to "reasonably assume"): a time series of +#' days in January would be encoded in a netCDF file as +#' `c("2024-01-01 12:00:00", "2024-01-02 12:00:00", "2024-01-03 12:00:00", ...)` +#' so `x <- c("2024-01-01", "2024-01-02", "2024-01-03")` would result in +#' `(NA, 1, 2)` (or `(NA, 1.5, 2.5)` with `method = "linear"`) because the date +#' values in `x` are at midnight. This situation is easily avoided by ensuring +#' that `y` has bounds set (use `bounds(y) <- TRUE` as a proximate solution if +#' bounds are not stored in the netCDF file). See the Examples. +#' +#' If bounds are set, the indices are taken from those bounds. Returned indices +#' may fall in between bounds if the latter are not contiguous, with the +#' exception of the extreme values in `x`. +#' +#' Values of `x` that are not valid timestamps according to the calendar of `y` +#' will be returned as `NA`. +#' +#' `x` can also be a numeric vector of index values, in which case the valid +#' values in `x` are returned. If negative values are passed, the positive +#' counterparts will be excluded and then the remainder returned. Positive and +#' negative values may not be mixed. Using a numeric vector has the side effect +#' that the result has the attribute "CFTime" describing the temporal dimension +#' of the slice. If index values outside of the range of `y` (`1:length(y)`) are +#' provided, an error will be thrown. +#' +#' @param x Vector of `character`, `POSIXt` or `Date` values to find indices +#' for, or a numeric vector. +#' @param y [CFTime] instance. +#' @param method Single value of "constant" or "linear". If `"constant"` or when +#' bounds are set on argument `y`, return the index value for each match. If +#' `"linear"`, return the index value with any fractional value. +#' +#' @returns A numeric vector giving indices into the "time" dimension of the +#' data set associated with `y` for the values of `x`. If there is at least 1 +#' valid index, then attribute "CFTime" contains an instance of `CFTime` that +#' describes the dimension of filtering the data set associated with `y` with +#' the result of this function, excluding any `NA`, `0` and +#' `.Machine$integer.max` values. +#' @export +#' +#' @examples +#' cf <- CFtime("days since 2020-01-01", "360_day", 1440:1799 + 0.5) +#' as_timestamp(cf)[1:3] +#' x <- c("2024-01-01", "2024-01-02", "2024-01-03") +#' indexOf(x, cf) +#' indexOf(x, cf, method = "linear") +#' +#' bounds(cf) <- TRUE +#' indexOf(x, cf) +#' +#' # Non-existent calendar day in a `360_day` calendar +#' x <- c("2024-03-30", "2024-03-31", "2024-04-01") +#' indexOf(x, cf) +#' +#' # Numeric x +#' indexOf(c(29, 30, 31), cf) +indexOf <- function(x, y, method = "constant") { + y$indexOf(x, method) +} + +#' Extreme time series values +#' +#' Character representation of the extreme values in the time series. +#' +#' @param x An instance of the [CFTime] class. +#' @param format A character string with format specifiers, optional. If it is +#' missing or an empty string, the most economical ISO8601 format is chosen: +#' "date" when no time information is present in `x`, "timestamp" otherwise. +#' Otherwise a suitable format specifier can be provided. +#' @param bounds Logical to indicate if the extremes from the bounds should be +#' used, if set. Defaults to `FALSE`. +#' @param ... Ignored. +#' @param na.rm Ignored. +#' @return Vector of two character representations of the extremes of the time +#' series. +#' @export +#' @examples +#' cf <- CFtime("days since 1850-01-01", "julian", 0:364) +#' range(cf) +#' range(cf, "%Y-%b-%e") +range.CFTime <- function(x, format = "", bounds = FALSE, ..., na.rm = FALSE) { + x$range(format, bounds) +} + +#' Indicates if the time series is complete +#' +#' This function indicates if the time series is complete, meaning that the time +#' steps are equally spaced and there are thus no gaps in the time series. +#' +#' This function gives exact results for time series where the nominal +#' *unit of separation* between observations in the time series is exact in +#' terms of the calendar unit. As an example, for a calendar unit of "days" where the +#' observations are spaced a fixed number of days apart the result is exact, but +#' if the same calendar unit is used for data that is on a monthly basis, the +#' *assessment* is approximate because the number of days per month is variable +#' and dependent on the calendar (the exception being the `360_day` calendar, +#' where the assessment is exact). The *result* is still correct in most cases +#' (including all CF-compliant data sets that the developers have seen) although +#' there may be esoteric constructions of CFTime and offsets that trip up this +#' implementation. +#' +#' @param x An instance of the [CFTime] class. +#' @returns logical. `TRUE` if the time series is complete, with no gaps; +#' `FALSE` otherwise. If no offsets have been added to the `CFTime` instance, +#' `NA` is returned. +#' @export +#' @examples +#' t <- CFtime("days since 1850-01-01", "julian", 0:364) +#' is_complete(t) +is_complete <- function(x) { + if (!inherits(x, "CFTime")) stop("Argument must be an instance of `CFTime`", call. = FALSE) + x$equidistant() +} + +#' Which time steps fall within two extreme values +#' +#' Given two extreme character timestamps, return a logical vector of a length +#' equal to the number of time steps in the [CFTime] instance with values `TRUE` +#' for those time steps that fall between the two extreme values, `FALSE` +#' otherwise. This can be used to select slices from the time series in reading +#' or analysing data. +#' +#' If bounds were set these will be preserved. +#' +#' @param x The `CFTime` instance to operate on. +#' @param extremes Character vector of two timestamps that represent the +#' extremes of the time period of interest. The timestamps must be in +#' increasing order. The timestamps need not fall in the range of the time +#' steps in argument `x. +#' @param rightmost.closed Is the larger extreme value included in the result? +#' Default is `FALSE`. +#' @returns A logical vector with a length equal to the number of time steps in +#' `x` with values `TRUE` for those time steps that fall between the two +#' extreme values, `FALSE` otherwise. The earlier timestamp is included, the +#' later timestamp is excluded. A specification of `c("2022-01-01", "2023-01-01")` +#' will thus include all time steps that fall in the year 2022. +#' @export +#' @examples +#' t <- CFtime("hours since 2023-01-01 00:00:00", "standard", 0:23) +#' slab(t, c("2022-12-01", "2023-01-01 03:00")) +slab <- function(x, extremes, rightmost.closed = FALSE) { + if (!inherits(x, "CFTime")) stop("First argument must be an instance of `CFTime`", call. = FALSE) + x$slab(extremes, rightmost.closed) +} + +#' Equivalence of CFTime objects +#' +#' This operator can be used to test if two [CFTime] objects represent the same +#' CF-convention time coordinates. Two `CFTime` objects are considered equivalent +#' if they have an equivalent calendar and the same offsets. +#' +#' @param e1,e2 Instances of the `CFTime` class. +#' @returns `TRUE` if the `CFTime` objects are equivalent, `FALSE` otherwise. +#' @export +#' @aliases CFtime-equivalent +#' @examples +#' e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) +#' e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 0:364) +#' e1 == e2 +"==.CFTime" <- function(e1, e2) + e1$cal$is_equivalent(e2$cal) && + length(e1$offsets) == length(e2$offsets) && + all(e1$offsets == e2$offsets) + +#' Extend a CFTime object +#' +#' A [CFTime] instance can be extended with this operator, using values from +#' another `CFTime` instance, or a vector of numeric offsets or character +#' timestamps. If the values come from another `CFTime` instance, the calendars +#' of the two instances must be compatible If the calendars of the `CFTime` +#' instances are not compatible, an error is thrown. +#' +#' The resulting `CFTime` instance will have the offsets of the original +#' `CFTime` instance, appended with offsets from argument `e2` in the order that +#' they are specified. If the new sequence of offsets is not monotonically +#' increasing a warning is generated (the COARDS metadata convention requires +#' offsets to be monotonically increasing). +#' +#' There is no reordering or removal of duplicates. This is because the time +#' series are usually associated with a data set and the correspondence between +#' the data in the files and the `CFTime` instance is thus preserved. When +#' merging the data sets described by this time series, the order must be +#' identical to the merging here. +#' +#' Note that when adding multiple vectors of offsets to a `CFTime` instance, it +#' is more efficient to first concatenate the vectors and then do a final +#' addition to the `CFTime` instance. So avoid +#' `CFtime(definition, calendar, e1) + CFtime(definition, calendar, e2) + CFtime(definition, calendar, e3) + ...` +#' but rather do `CFtime(definition, calendar) + c(e1, e2, e3, ...)`. It is the +#' responsibility of the operator to ensure that the offsets of the different +#' data sets are in reference to the same calendar. +#' +#' Note also that `RNetCDF` and `ncdf4` packages both return the values of the +#' "time" dimension as a 1-dimensional array. You have to `dim(time_values) <- +#' NULL` to de-class the array to a vector before adding offsets to an existing +#' `CFtime` instance. +#' +#' Any bounds that were set will be removed. Use [bounds()] to retrieve the +#' bounds of the individual `CFTime` instances and then set them again after +#' merging the two instances. +#' +#' @param e1 Instance of the `CFTime` class. +#' @param e2 Instance of the `CFTime` class with a calendar compatible with that +#' of argument `e1`, or a numeric vector with offsets from the origin of +#' argument `e1`, or a vector of `character` timestamps in ISO8601 or UDUNITS +#' format. +#' @returns A `CFTime` object with the offsets of argument `e1` extended by the +#' values from argument `e2`. +#' @export +#' @aliases CFtime-merge +#' @examples +#' e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) +#' e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 365:729) +#' e1 + e2 +"+.CFTime" <- function(e1, e2) { + if (inherits(e2, "CFTime")) { + if (!e1$cal$is_compatible(e2$cal)) stop("Calendars not compatible", call. = FALSE) + if (all(e1$cal$origin[1:6] == e2$cal$origin[1:6])) + CFTime$new(e1$cal$definition, e1$cal$name, c(e1$offsets, e2$offsets)) + else { + diff <- e1$cal$parse(paste(e2$cal$origin_date, e2$cal$origin_time))$offset + CFTime$new(e1$cal$definition, e1$cal$name, c(e1$offsets, e2$offsets + diff)) + } + } else if (is.numeric(e2) && .validOffsets(e2)) { + CFTime$new(e1$cal$definition, e1$cal$name, c(e1$offsets, e2)) + } else { + time <- e1$cal$parse(e2) + if (anyNA(time$year)) stop("Argument `e2` contains invalid timestamps", call. = FALSE) + CFTime$new(e1$cal$definition, e1$cal$name, c(e1$offsets, time$offset)) + } +} + +# ============================================================================== +# Factors and coverage + +#' Create a factor from the offsets in a `CFTime` instance +#' +#' With this function a factor can be generated for the time series, or a part +#' thereof, contained in the [CFTime] instance. This is specifically interesting +#' for creating factors from the date part of the time series that aggregate the +#' time series into longer time periods (such as month) that can then be used to +#' process daily CF data sets using, for instance, `tapply()`. +#' +#' The factor will respect the calendar that the time series is built on. For +#' `period`s longer than a day this will result in a factor where the calendar +#' is no longer relevant (because calendars impacts days, not dekads, months, +#' quarters, seasons or years). +#' +#' The factor will be generated in the order of the offsets of the `CFTime` +#' instance. While typical CF-compliant data sources use ordered time series +#' there is, however, no guarantee that the factor is ordered as multiple +#' `CFTime` objects may have been merged out of order. For most processing with +#' a factor the ordering is of no concern. +#' +#' If the `era` parameter is specified, either as a vector of years to include +#' in the factor, or as a list of such vectors, the factor will only consider +#' those values in the time series that fall within the list of years, inclusive +#' of boundary values. Other values in the factor will be set to `NA`. The years +#' need not be contiguous, within a single vector or among the list items, or in +#' order. +#' +#' The following periods are supported by this function: +#' +#' \itemize{ +#' \item `year`, the year of each offset is returned as "YYYY". +#' \item `season`, the meteorological season of each offset is returned as +#' "Sx", with x being 1-4, preceeded by "YYYY" if no `era` is +#' specified. Note that December dates are labeled as belonging to the +#' subsequent year, so the date "2020-12-01" yields "2021S1". This implies +#' that for standard CMIP files having one or more full years of data the +#' first season will have data for the first two months (January and +#' February), while the final season will have only a single month of data +#' (December). +#' \item `quarter`, the calendar quarter of each offset is returned as "Qx", +#' with x being 1-4, preceeded by "YYYY" if no `era` is specified. +#' \item `month`, the month of each offset is returned as "01" to +#' "12", preceeded by "YYYY-" if no `era` is specified. This is the default +#' period. +#' \item `dekad`, ten-day periods are returned as +#' "Dxx", where xx runs from "01" to "36", preceeded by "YYYY" if no `era` +#' is specified. Each month is subdivided in dekads as follows: 1- days 01 - +#' 10; 2- days 11 - 20; 3- remainder of the month. +#' \item `day`, the month and day of each offset are returned as "MM-DD", +#' preceeded by "YYYY-" if no `era` is specified. +#' } +#' +#' It is not possible to create a factor for a period that is shorter than the +#' temporal resolution of the source data set from which the `t` argument +#' derives. As an example, if the source data set has monthly data, a dekad or +#' day factor cannot be created. +#' +#' Creating factors for other periods is not supported by this function. Factors +#' based on the timestamp information and not dependent on the calendar can +#' trivially be constructed from the output of the [as_timestamp()] function. +#' +#' For non-era factors the attribute 'CFTime' of the result contains a `CFTime` +#' instance that is valid for the result of applying the factor to a data set +#' that the `t` argument is associated with. In other words, if `CFTime` +#' instance 'At' describes the temporal dimension of data set 'A' and a factor +#' 'Af' is generated like `Af <- CFfactor(At)`, then `Bt <- attr(Af, "CFTime")` +#' describes the temporal dimension of the result of, say, +#' `B <- apply(A, 1:2, tapply, Af, FUN)`. The 'CFTime' attribute is `NULL` for +#' era factors. +#' +#' @param t An instance of the `CFTime` class whose offsets will be used to +#' construct the factor. +#' @param period character. A character string with one of the values "year", +#' "season", "quarter", "month" (the default), "dekad" or "day". +#' @param era numeric or list, optional. Vector of years for which to +#' construct the factor, or a list whose elements are each a vector of years. +#' If `era` is not specified, the factor will use the entire time series for +#' the factor. +#' +#' @returns If `era` is a single vector or not specified, a factor with a +#' length equal to the number of offsets in `t`. If `era` is a list, a list +#' with the same number of elements and names as `era`, each containing a +#' factor. Elements in the factor will be set to `NA` for time series values +#' outside of the range of specified years. +#' +#' The factor, or factors in the list, have attributes 'period', 'era' and +#' 'CFTime'. Attribute 'period' holds the value of the `period` argument. +#' Attribute 'era' indicates the number of years that are included in the +#' era, or -1 if no `era` is provided. Attribute 'CFTime' holds an +#' instance of `CFTime` that has the same definition as `t`, but with offsets +#' corresponding to the mid-point of non-era factor levels; if the `era` +#' argument is specified, attribute 'CFTime' is `NULL`. +#' @seealso [cut()] creates a non-era factor for arbitrary cut points. +#' @export +#' +#' @examples +#' t <- CFtime("days since 1949-12-01", "360_day", 19830:54029) +#' +#' # Create a dekad factor for the whole time series +#' f <- CFfactor(t, "dekad") +#' +#' # Create three monthly factors for early, mid and late 21st century eras +#' ep <- CFfactor(t, era = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) +CFfactor <- function(t, period = "month", era = NULL) { + if (!(inherits(t, "CFTime"))) stop("First argument to CFfactor() must be an instance of the `CFTime` class", call. = FALSE) + t$factor(period, era) +} + +#' Number of base time units in each factor level +#' +#' Given a factor as returned by [CFfactor()] and the [CFTime] instance from +#' which the factor was derived, this function will return a numeric vector with +#' the number of time units in each level of the factor. +#' +#' The result of this function is useful to convert between absolute and +#' relative values. Climate change anomalies, for instance, are usually computed +#' by differencing average values between a future period and a baseline period. +#' Going from average values back to absolute values for an aggregate period +#' (which is typical for temperature and precipitation, among other variables) +#' is easily done with the result of this function, without having to consider +#' the specifics of the calendar of the data set. +#' +#' If the factor `f` is for an era (e.g. spanning multiple years and the +#' levels do not indicate the specific year), then the result will indicate the +#' number of time units of the period in a regular single year. In other words, +#' for an era of 2041-2060 and a monthly factor on a standard calendar with a +#' `days` unit, the result will be `c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)`. +#' Leap days are thus only considered for the `366_day` and `all_leap` calendars. +#' +#' Note that this function gives the number of time units in each level of the +#' factor - the actual number of data points in the `cf` instance per factor +#' level may be different. Use [CFfactor_coverage()] to determine the actual +#' number of data points or the coverage of data points relative to the factor +#' level. +#' +#' @param t An instance of `CFTime`. +#' @param f A factor or a list of factors derived from the +#' parameter `t`. The factor or list thereof should generally be generated by +#' the function [CFfactor()]. +#' +#' @returns If `f` is a factor, a numeric vector with a length equal to the +#' number of levels in the factor, indicating the number of time units in each +#' level of the factor. If `f` is a list of factors, a list with each element +#' a numeric vector as above. +#' @export +#' +#' @examples +#' t <- CFtime("days since 2001-01-01", "365_day", 0:364) +#' f <- CFfactor(t, "dekad") +#' CFfactor_units(t, f) +CFfactor_units <- function(t, f) { + if (!inherits(t, "CFTime")) stop("First argument to `CFfactor_units()` must be an instance of the `CFTime` class", call. = FALSE) + t$factor_units(f) +} + +#' Coverage of time elements for each factor level +#' +#' This function calculates the number of time elements, or the relative +#' coverage, in each level of a factor generated by [CFfactor()]. +#' +#' @param t An instance of [CFTime]. +#' @param f factor or list. A factor or a list of factors derived from the +#' parameter `t`. The factor or list thereof should generally be generated by +#' the function [CFfactor()]. +#' @param coverage "absolute" or "relative". +#' @returns If `f` is a factor, a numeric vector with a length equal to the +#' number of levels in the factor, indicating the number of units from the +#' time series in `t` contained in each level of the factor when +#' `coverage = "absolute"` or the proportion of units present relative to the +#' maximum number when `coverage = "relative"`. If `f` is a list of factors, a +#' list with each element a numeric vector as above. +#' @export +#' +#' @examples +#' t <- CFtime("days since 2001-01-01", "365_day", 0:364) +#' f <- CFfactor(t, "dekad") +#' CFfactor_coverage(t, f, "absolute") +CFfactor_coverage <- function(t, f, coverage = "absolute") { + if (!inherits(t, "CFTime")) stop("First argument to `CFfactor_coverage()` must be an instance of the `CFTime` class", call. = FALSE) + t$factor_coverage(f, coverage) +} + +# ============================================================================== +# Regular functions + +#' Create a vector that represents CF timestamps +#' +#' This function generates a vector of character strings or `POSIXct`s that +#' represent the date and time in a selectable combination for each offset. +#' +#' The character strings use the format `YYYY-MM-DDThh:mm:ss±hhmm`, depending on +#' the `format` specifier. The date in the string is not necessarily compatible +#' with `POSIXt` - in the `360_day` calendar `2017-02-30` is valid and +#' `2017-03-31` is not. +#' +#' For the "proleptic_gregorian" calendar the output can also be generated as a +#' vector of `POSIXct` values by specifying `asPOSIX = TRUE`. The same is +#' possible for the "standard" and "gregorian" calendars but only if all +#' timestamps fall on or after 1582-10-15. +#' +#' @param t The `CFTime` instance that contains the offsets to use. +#' @param format character. A character string with either of the values "date" +#' or "timestamp". If the argument is not specified, the format used is +#' "timestamp" if there is time information, "date" otherwise. +#' @param asPOSIX logical. If `TRUE`, for "standard", "gregorian" and +#' "proleptic_gregorian" calendars the output is a vector of `POSIXct` - for +#' other calendars an error will be thrown. Default value is `FALSE`. +#' @seealso The [CFTime] `format()` method gives greater flexibility through +#' the use of `strptime`-like format specifiers. +#' @returns A character vector where each element represents a moment in time +#' according to the `format` specifier. +#' @export +#' @examples +#' t <- CFtime("hours since 2020-01-01", "standard", seq(0, 24, by = 0.25)) +#' as_timestamp(t, "timestamp") +#' +#' t2 <- CFtime("days since 2002-01-21", "standard", 0:20) +#' tail(as_timestamp(t2, asPOSIX = TRUE)) +#' +#' tail(as_timestamp(t2)) +#' +#' tail(as_timestamp(t2 + 1.5)) +as_timestamp <- function(t, format = NULL, asPOSIX = FALSE) { + if (!(inherits(t, "CFTime"))) + stop("First argument to `as_timestamp()` must be an instance of the `CFTime` class", call. = FALSE) + t$as_timestamp(format, asPOSIX) +} + +#' Return the number of days in a month given a certain CF calendar +#' +#' Given a vector of dates as strings in ISO 8601 or UDUNITS format and a +#' [CFTime] object, this function will return a vector of the same length as the +#' dates, indicating the number of days in the month according to the calendar +#' specification. If no vector of days is supplied, the function will return an +#' integer vector of length 12 with the number of days for each month of the +#' calendar (disregarding the leap day for `standard` and `julian` calendars). +#' +#' @param t The `CFtime` instance to use. +#' @param x character. An optional vector of dates as strings with format +#' `YYYY-MM-DD`. Any time part will be silently ingested. +#' +#' @returns A vector indicating the number of days in each month for the vector +#' of dates supplied as argument `x. Invalidly specified dates will result in +#' an `NA` value. If no dates are supplied, the number of days per month for +#' the calendar as a vector of length 12. +#' +#' @export +#' @seealso When working with factors generated by [CFfactor()], it is usually +#' better to use [CFfactor_units()] as that will consider leap days for +#' non-era factors. [CFfactor_units()] can also work with other time periods +#' and calendar units, such as "hours per month", or "days per season". +#' @examples +#' dates <- c("2021-11-27", "2021-12-10", "2022-01-14", "2022-02-18") +#' t <- CFtime("days since 1850-01-01", "standard") +#' month_days(t, dates) +#' +#' t <- CFtime("days since 1850-01-01", "360_day") +#' month_days(t, dates) +#' +#' t <- CFtime("days since 1850-01-01", "all_leap") +#' month_days(t, dates) +#' +#' month_days(t) +month_days <- function(t, x = NULL) { + stopifnot(inherits(t, "CFTime")) + + if (is.null(x)) + return(t$cal$month_days()) + else { + if (!(is.character(x))) stop("Argument `x` must be a character vector of dates in 'YYYY-MM-DD' format") + + ymd <- t$cal$parse(x) + if (anyNA(ymd$year)) warning("Some dates could not be parsed. Result contains `NA` values.", call. = FALSE) + return(t$cal$month_days(ymd)) + } +} + +#' Parse series of timestamps in CF format to date-time elements +#' +#' This function will parse a vector of timestamps in ISO8601 or UDUNITS format +#' into a data frame with columns for the elements of the timestamp: year, +#' month, day, hour, minute, second, time zone. Those timestamps that could not +#' be parsed or which represent an invalid date in the indicated `CFtime` +#' instance will have `NA` values for the elements of the offending timestamp +#' (which will generate a warning). +#' +#' The supported formats are the *broken timestamp* format from the UDUNITS +#' library and ISO8601 *extended*, both with minor changes, as suggested by the +#' CF Metadata Conventions. In general, the format is `YYYY-MM-DD hh:mm:ss.sss +#' hh:mm`. The year can be from 1 to 4 digits and is interpreted literally, so +#' `79-10-24` is the day Mount Vesuvius erupted and destroyed Pompeii, not +#' `1979-10-24`. The year and month are mandatory, all other fields are +#' optional. There are defaults for all missing values, following the UDUNITS +#' and CF Metadata Conventions. Leading zeros can be omitted in the UDUNITS +#' format, but not in the ISO8601 format. The optional fractional part can have +#' as many digits as the precision calls for and will be applied to the smallest +#' specified time unit. In the result of this function, if the fraction is +#' associated with the minute or the hour, it is converted into a regular +#' `hh:mm:ss.sss` format, i.e. any fraction in the result is always associated +#' with the second, rounded down to milli-second accuracy. The separator between +#' the date and the time can be a single whitespace character or a `T`. +#' +#' The time zone is optional and should have at least the hour or `Z` if +#' present, the minute is optional. The time zone hour can have an optional +#' sign. In the UDUNITS format the separator between the time and the time zone +#' must be a single whitespace character, in ISO8601 there is no separation +#' between the time and the timezone. Time zone names are not supported (as +#' neither UDUNITS nor ISO8601 support them) and will cause parsing to fail when +#' supplied, with one exception: the designator "UTC" is silently dropped (i.e. +#' interpreted as "00:00"). +#' +#' Currently only the extended formats (with separators between the elements) +#' are supported. The vector of timestamps may have any combination of ISO8601 +#' and UDUNITS formats. +#' +#' @param t An instance of `CFTime` to use when parsing the date. +#' @param x Vector of character strings representing timestamps in +#' ISO8601 extended or UDUNITS broken format. +#' @returns A `data.frame` with constituent elements of the parsed timestamps in +#' numeric format. The columns are year, month, day, hour, minute, second +#' (with an optional fraction), time zone (character string), and the +#' corresponding offset value from the origin. Invalid input data will appear +#' as `NA` - if this is the case, a warning message will be displayed - other +#' missing information on input will use default values. +#' @export +#' @examples +#' t <- CFtime("days since 0001-01-01", "proleptic_gregorian") +#' +#' # This will have `NA`s on output and generate a warning +#' timestamps <- c("2012-01-01T12:21:34Z", "12-1-23", "today", +#' "2022-08-16T11:07:34.45-10", "2022-08-16 10.5+04") +#' parse_timestamps(t, timestamps) +parse_timestamps <- function(t, x) { + stopifnot(is.character(x), inherits(t, "CFTime")) + if (t$cal$unit > 4) stop("Parsing of timestamps on a 'month' or 'year' time unit is not supported.", call. = FALSE) + + out <- t$cal$parse(x) + if (anyNA(out$year)) + warning("Some dates could not be parsed. Result contains `NA` values.") + if (length(unique(out$tz)) > 1) + warning("Timestamps have multiple time zones. Some or all may be different from the calendar time zone.") + else if (out$tz[1] != t$cal$timezone) + warning("Timestamps have time zone that is different from the calendar.") + out +} + diff --git a/R/deprecated.R b/R/deprecated.R index 3900111..04d8600 100644 --- a/R/deprecated.R +++ b/R/deprecated.R @@ -11,26 +11,27 @@ #' | ------------------- | -------------------- | #' | CFcomplete() | [is_complete()] | #' | CFmonth_days() | [month_days()] | +#' | CFparse() | [parse_timestamps()] | #' | CFrange() | [range()] | #' | CFsubset() | [slab()] | #' | CFtimestamp() | [as_timestamp()] | #' -#' @param cf,x,format,asPOSIX See replacement functions. +#' @param t,x,format,asPOSIX,extremes See replacement functions. #' #' @returns See replacement functions. #' @rdname deprecated_functions #' @export -CFtimestamp <- function(cf, format = NULL, asPOSIX = FALSE) { +CFtimestamp <- function(t, format = NULL, asPOSIX = FALSE) { warning("Function `CFtimestamp()` is deprecated. Use `as_timestamp()` instead.") - as_timestamp(cf, format, asPOSIX) + as_timestamp(t, format, asPOSIX) } #' @rdname deprecated_functions #' @export -CFmonth_days <- function(cf, x = NULL) { +CFmonth_days <- function(t, x = NULL) { warning("Function `CFmonth_days()` is deprecated. Use `month_days()` instead.") - month_days(cf, x) + month_days(t, x) } #' @rdname deprecated_functions @@ -40,7 +41,16 @@ CFcomplete <- function(x) { is_complete(x) } +#' @rdname deprecated_functions +#' @export CFsubset <- function(x, extremes) { warning("Function `CFsubset()` is deprecated. Use `slab()` instead.") slab(x, extremes) } + +#' @rdname deprecated_functions +#' @export +CFparse <- function(t, x) { + warning("Function `CFparse()` is deprecated. Use `parse_timestamps()` instead.") + parse_timestamps(t, x) +} diff --git a/R/helpers.R b/R/helpers.R new file mode 100644 index 0000000..e2f8eb0 --- /dev/null +++ b/R/helpers.R @@ -0,0 +1,175 @@ +# Internal functions +# +# The functions in this source file are for internal use only. + +# ============================================================================== +# Offsets and timestamp formatting + +#' Validate offsets passed into a CFTime instance +#' +#' Tests the `offsets` values. Throws an error if the argument contains `NA` values. +#' +#' @param offsets The offsets to test +#' +#' @returns logical. `TRUE` if the offsets are valid, throws an error otherwise. +#' @noRd +.validOffsets <- function(offsets) { + if (any(is.na(offsets))) stop("Offsets cannot contain `NA` values.", call. = FALSE) + TRUE +} + +#' Formatting of time strings from time elements +#' +#' This is an internal function that should not generally be used outside of +#' the CFtime package. +#' +#' @param t A `data.frame` representing timestamps. +#' +#' @returns A vector of character strings with a properly formatted time. If any +#' timestamp has a fractional second part, then all time strings will report +#' seconds at milli-second precision. +#' @noRd +.format_time <- function(t) { + fsec <- t$second %% 1L + if (any(fsec > 0L)) { + paste0(sprintf("%02d:%02d:", t$hour, t$minute), ifelse(t$second < 10, "0", ""), sprintf("%.3f", t$second)) + } else { + sprintf("%02d:%02d:%02d", t$hour, t$minute, t$second) + } +} + +#' Do the time elements have time-of-day information? +#' +#' If any time information > 0, then `TRUE` otherwise `FALSE`. +#' +#' This is an internal function that should not generally be used outside of +#' the CFtime package. +#' +#' @param t A `data.frame` representing timestamps. +#' +#' @returns `TRUE` if any timestamp has time-of-day information, `FALSE` otherwise. +#' @noRd +.has_time <- function(t) { + any(t$hour > 0) || any(t$minute > 0) || any(t$second > 0) +} + +#' Do formatting of timestamps with format specifiers +#' +#' @param ts `data.frame` of decomposed offsets. +#' @param tz Time zone character string. +#' @param format A character string with the format specifiers, or +#' "date" or "timestamp". +#' @returns Character vector of formatted timestamps. +#' @noRd +.format_format <- function(ts, tz, format) { + if (format == "") format <- "timestamp" + if (format == "timestamp" && sum(ts$hour, ts$minute, ts$second) == 0) + format <- "date" + + if (format == "date") return(sprintf("%04d-%02d-%02d", ts$year, ts$month, ts$day)) + else if (format == "timestamp") return(sprintf("%04d-%02d-%02d %s", ts$year, ts$month, ts$day, .format_time(ts))) + + # Expand any composite specifiers + format <- stringr::str_replace_all(format, c("%F" = "%Y-%m-%d", "%R" = "%H:%M", "%T" = "%H:%M:%S")) + + # Splice in timestamp values for specifiers + # nocov start + if (grepl("%b|%h", format[1])) { + mon <- strftime(ISOdatetime(2024, 1:12, 1, 0, 0, 0), "%b") + format <- stringr::str_replace_all(format, "%b|%h", mon[ts$month]) + } + if (grepl("%B", format[1])) { + mon <- strftime(ISOdatetime(2024, 1:12, 1, 0, 0, 0), "%B") + format <- stringr::str_replace_all(format, "%B", mon[ts$month]) + } + # nocov end + format <- stringr::str_replace_all(format, "%[O]?d", sprintf("%02d", ts$day)) + format <- stringr::str_replace_all(format, "%e", sprintf("%2d", ts$day)) + format <- stringr::str_replace_all(format, "%[O]?H", sprintf("%02d", ts$hour)) + format <- stringr::str_replace_all(format, "%[O]?I", sprintf("%02d", ts$hour %% 12)) + format <- stringr::str_replace_all(format, "%[O]?m", sprintf("%02d", ts$month)) + format <- stringr::str_replace_all(format, "%[O]?M", sprintf("%02d", ts$minute)) + format <- stringr::str_replace_all(format, "%p", ifelse(ts$hour < 12, "AM", "PM")) + format <- stringr::str_replace_all(format, "%S", sprintf("%02d", as.integer(ts$second))) + format <- stringr::str_replace_all(format, "%[E]?Y", sprintf("%04d", ts$year)) + format <- stringr::str_replace_all(format, "%z", tz) + format <- stringr::str_replace_all(format, "%%", "%") + format +} + +# ============================================================================== +# Other internal functions + +#' Calculate time units in factors +#' +#' @param f factor. Factor as generated by `CFfactor()`. +#' @param cal `CFCalendar` instance of the `CFTime` instance. +#' @param upd numeric. Number of units per day, from the `CFt` environment. +#' @returns A vector as long as the number of levels in the factor. +#' @noRd +.factor_units <- function(f, cal, upd) { + period <- attr(f, "period") + cal_class <- class(cal)[1L] + + res <- if (period == "day") + rep(1L, nlevels(f)) + else if (cal_class == "CFCalendar360") { + rep(c(360L, 90L, 90L, 30L, 10L, 1L)[which(CFt$factor_periods == period)], nlevels(f)) + } else { + if (attr(f, "era") > 0L) { + if (cal_class == "CFCalendar366") { + switch(period, + "year" = rep(366L, nlevels(f)), + "season" = c(91L, 92L, 92L, 91L)[as.integer(substr(levels(f), 2, 2))], + "quarter" = c(91L, 91L, 92L, 92L)[as.integer(levels(f))], + "month" = c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[as.integer(levels(f))], + "dekad" = { + dk <- as.integer(substr(levels(f), 2L, 3L)) + ifelse(dk %% 3L > 0L | dk %in% c(12L, 18L, 27L, 33L), 10L, + ifelse(dk %in% c(3L, 9L, 15L, 21L, 24L, 30L, 36L), 11L, 9L)) + } + ) + } else { + switch(period, + "year" = rep(365L, nlevels(f)), + "season" = c(90L, 92L, 92L, 91L)[as.integer(substr(levels(f), 2, 2))], + "quarter" = c(90L, 91L, 92L, 92L)[as.integer(substr(levels(f), 2, 2))], + "month" = c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[as.integer(levels(f))], + "dekad" = { + dk <- as.integer(substr(levels(f), 2L, 3L)) + ifelse(dk %% 3L > 0L | dk %in% c(12L, 18L, 27L, 33L), 10L, + ifelse(dk %in% c(3L, 9L, 15L, 21L, 24L, 30L, 36L), 11L, 8L)) + } + ) + } + } else { # not an era factor + switch(period, + "year" = ifelse(cal$leap_year(as.integer(levels(f))), 366L, 365L), + "season" = { + year <- as.integer(substr(levels(f), 1L, 4L)) + season <- as.integer(substr(levels(f), 6L, 6L)) + ifelse(cal$leap_year(year), c(91L, 92L, 92L, 91L)[season], c(90L, 92L, 92L, 91L)[season]) + }, + "quarter" = { + year <- as.integer(substr(levels(f), 1L, 4L)) + qtr <- as.integer(substr(levels(f), 6L, 6L)) + ifelse(cal$leap_year(year), c(91L, 91L, 92L, 92L)[qtr], c(90L, 91L, 92L, 92L)[qtr]) + }, + "month" = { + year <- as.integer(substr(levels(f), 1L, 4L)) + month <- as.integer(substr(levels(f), 6L, 7L)) + ifelse(cal$leap_year(year), c(31L, 29L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[month], + c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L)[month]) + }, + "dekad" = { + year <- as.integer(substr(levels(f), 1L, 4L)) + dk <- as.integer(substr(levels(f), 6L, 7L)) + ifelse(dk %% 3L > 0L | dk %in% c(12L, 18L, 27L, 33L), 10L, + ifelse(dk %in% c(3L, 9L, 15L, 21L, 24L, 30L, 36L), 11L, + ifelse(cal$leap_year(year), 9L, 8L))) + } + ) + } + } + res * upd +} diff --git a/R/zzz.R b/R/zzz.R index c69a0ff..c28da6b 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -3,8 +3,6 @@ CFt <- new.env(parent = emptyenv()) .onLoad <- function(libname, pkgname) { - assign("calendars", data.frame(name = c("standard", "gregorian", "proleptic_gregorian", "julian", "360_day", "365_day", "366_day", "noleap", "all_leap"), - id = c(1L, 1L, 1L, 2L, 3L, 4L, 5L, 4L, 5L)), envir = CFt) assign("CFunits", data.frame(unit = c("years", "year", "yr", "months", "month", "mon", "days", "day", "d", "hours", "hour", "hr", "h", "minutes", "minute", "min", "seconds", "second", "sec", "s"), id = c(6L, 6L, 6L, 5L, 5L, 5L, 4L, 4L, 4L, 3L, 3L, 3L, 3L, 2L, 2L, 2L, 1L, 1L, 1L, 1L)), envir = CFt) assign("units", data.frame(name = c("seconds", "minutes", "hours", "days", "months", "years"), diff --git a/README.Rmd b/README.Rmd index 280b9de..bd38c4c 100644 --- a/README.Rmd +++ b/README.Rmd @@ -161,5 +161,6 @@ This package has been tested with the following data sets: - CMIP5 - CORDEX - CMIP6 +- ROMS The package also operates on geographical and/or temporal subsets of data sets so long as the subsetted data complies with the CF Metadata Conventions. This includes subsetting in the [Climate Data Store](https://cds.climate.copernicus.eu/#!/home). Subsetted data from Climate4Impact is not automatically supported because the dimension names are not compliant with the CF Metadata Conventions, use the corresponding dimension names instead. diff --git a/README.md b/README.md index 85b4eec..5059b43 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ This package has been tested with the following data sets: - CMIP5 - CORDEX - CMIP6 +- ROMS The package also operates on geographical and/or temporal subsets of data sets so long as the subsetted data complies with the CF Metadata diff --git a/man/CFCalendar.Rd b/man/CFCalendar.Rd new file mode 100644 index 0000000..e7320bd --- /dev/null +++ b/man/CFCalendar.Rd @@ -0,0 +1,282 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CFCalendar.R +\docType{class} +\name{CFCalendar} +\alias{CFCalendar} +\title{Basic CF calendar} +\description{ +This class represents a basic CF calendar. It should not be +instantiated directly; instead, use one of the descendant classes. + +This internal class stores the information to represent date and time +values using the CF conventions. An instance is created by the exported +\link{CFTime} class, which also exposes the relevant properties of this class. + +The following calendars are supported: + +\itemize{ +\item \code{\link[=CFCalendarStandard]{gregorian\\standard}}, the international standard calendar for civil use. +\item \code{\link[=CFCalendarProleptic]{proleptic_gregorian}}, the standard calendar but extending before 1582-10-15 +when the Gregorian calendar was adopted. +\item \code{\link[=CFCalendarJulian]{julian}}, every fourth year is a leap year (so including the years 1700, 1800, 1900, 2100, etc). +\item \code{\link[=CFCalendar365]{noleap\\365_day}}, all years have 365 days. +\item \code{\link[=CFCalendar366]{all_leap\\366_day}}, all years have 366 days. +\item \code{\link[=CFCalendar360]{360_day}}, all years have 360 days, divided over 12 months of 30 days. +} +} +\references{ +https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#calendar +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{name}}{Descriptive name of the calendar, as per the CF Metadata +Conventions.} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} + +\item{\code{unit}}{The numeric id of the unit of the calendar.} + +\item{\code{origin}}{\code{data.frame} with fields for the origin of the calendar.} +} +\if{html}{\out{
}} +} +\section{Active bindings}{ +\if{html}{\out{
}} +\describe{ +\item{\code{origin_date}}{(read-only) Character string with the date of the +calendar.} + +\item{\code{origin_time}}{(read-only) Character string with the time of the +calendar.} + +\item{\code{timezone}}{(read-only) Character string with the time zone of the +origin of the calendar.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFCalendar-new}{\code{CFCalendar$new()}} +\item \href{#method-CFCalendar-print}{\code{CFCalendar$print()}} +\item \href{#method-CFCalendar-valid_days}{\code{CFCalendar$valid_days()}} +\item \href{#method-CFCalendar-POSIX_compatible}{\code{CFCalendar$POSIX_compatible()}} +\item \href{#method-CFCalendar-is_compatible}{\code{CFCalendar$is_compatible()}} +\item \href{#method-CFCalendar-is_equivalent}{\code{CFCalendar$is_equivalent()}} +\item \href{#method-CFCalendar-parse}{\code{CFCalendar$parse()}} +\item \href{#method-CFCalendar-offsets2time}{\code{CFCalendar$offsets2time()}} +\item \href{#method-CFCalendar-clone}{\code{CFCalendar$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-new}{}}} +\subsection{Method \code{new()}}{ +Create a new CF calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$new(nm, definition)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{nm}}{The name of the calendar. This must follow the CF Metadata +Conventions.} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-print}{}}} +\subsection{Method \code{print()}}{ +Print information about the calendar to the console. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$print(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{Ignored.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{self}, invisibly. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-valid_days}{}}} +\subsection{Method \code{valid_days()}}{ +Indicate which of the supplied dates are valid. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$valid_days(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{NULL}. A warning will be generated to the effect that a +descendant class should be used for this method. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-POSIX_compatible}{}}} +\subsection{Method \code{POSIX_compatible()}}{ +Indicate if the time series described using this calendar +can be safely converted to a standard date-time type (\code{POSIXct}, +\code{POSIXlt}, \code{Date}). + +Only the 'standard' calendar and the 'proleptic_gregorian' calendar +when all dates in the time series are more recent than 1582-10-15 +(inclusive) can be safely converted, so this method returns \code{FALSE} by +default to cover the majority of cases. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$POSIX_compatible(offsets)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{offsets}}{The offsets from the CFtime instance.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{FALSE} by default. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-is_compatible}{}}} +\subsection{Method \code{is_compatible()}}{ +This method tests if the \code{CFCalendar} instance in argument +\code{cal} is compatible with \code{self}, meaning that they are of the same +class and have the same unit. Calendars "standard", and "gregorian" are +compatible, as are the pairs of "365_day" and "no_leap", and "366_day" +and "all_leap". +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$is_compatible(cal)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{cal}}{Instance of a descendant of the \code{CFCalendar} class.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{TRUE} if the instance in argument \code{cal} is compatible with +\code{self}, \code{FALSE} otherwise. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-is_equivalent}{}}} +\subsection{Method \code{is_equivalent()}}{ +This method tests if the \code{CFCalendar} instance in argument +\code{cal} is equivalent to \code{self}, meaning that they are of the same class, +have the same unit, and equivalent origins. Calendars "standard", and +"gregorian" are equivalent, as are the pairs of "365_day" and +"no_leap", and "366_day" and "all_leap". + +Note that the origins need not be identical, but their parsed values +have to be. "2000-01" is parsed the same as "2000-01-01 00:00:00", for +instance. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$is_equivalent(cal)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{cal}}{Instance of a descendant of the \code{CFCalendar} class.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{TRUE} if the instance in argument \code{cal} is equivalent to +\code{self}, \code{FALSE} otherwise. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-parse}{}}} +\subsection{Method \code{parse()}}{ +Parsing a vector of date-time character strings into parts. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$parse(d)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{d}}{character. A character vector of date-times.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns year, month, day, hour, minute, +second, time zone, and offset. Invalid input data will appear as \code{NA}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-offsets2time}{}}} +\subsection{Method \code{offsets2time()}}{ +Decompose a vector of offsets, in units of the calendar, to +their timestamp values. This adds a specified amount of time to the +origin of a \code{CFTime} object. + +This method may introduce inaccuracies where the calendar unit is +"months" or "years", due to the ambiguous definition of these units. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$offsets2time(offsets)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{offsets}}{Vector of numeric offsets to add to the origin of the +calendar.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns for the timestamp elements and as +many rows as there are offsets. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/CFCalendar360.Rd b/man/CFCalendar360.Rd new file mode 100644 index 0000000..1fedfb0 --- /dev/null +++ b/man/CFCalendar360.Rd @@ -0,0 +1,197 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CFCalendar360.R +\docType{class} +\name{CFCalendar360} +\alias{CFCalendar360} +\title{360-day CF calendar} +\description{ +This class represents a CF calendar of 360 days per year, evenly +divided over 12 months of 30 days. This calendar is obviously not +compatible with the standard POSIXt calendar. + +This calendar supports dates before year 1 and includes the year 0. +} +\section{Super class}{ +\code{\link[CFtime:CFCalendar]{CFtime::CFCalendar}} -> \code{CFCalendar360} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFCalendar360-new}{\code{CFCalendar360$new()}} +\item \href{#method-CFCalendar360-valid_days}{\code{CFCalendar360$valid_days()}} +\item \href{#method-CFCalendar360-month_days}{\code{CFCalendar360$month_days()}} +\item \href{#method-CFCalendar360-leap_year}{\code{CFCalendar360$leap_year()}} +\item \href{#method-CFCalendar360-date2offset}{\code{CFCalendar360$date2offset()}} +\item \href{#method-CFCalendar360-offset2date}{\code{CFCalendar360$offset2date()}} +\item \href{#method-CFCalendar360-clone}{\code{CFCalendar360$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar360-new}{}}} +\subsection{Method \code{new()}}{ +Create a new CF calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar360$new(nm, definition)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{nm}}{The name of the calendar. This must be "360_day". This argument +is superfluous but maintained to be consistent with the initialization +methods of the parent and sibling classes.} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new instance of this class. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar360-valid_days}{}}} +\subsection{Method \code{valid_days()}}{ +Indicate which of the supplied dates are valid. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar360$valid_days(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{ymd} has rows +with \code{TRUE} for valid days and \code{FALSE} for invalid days, or \code{NA} where +the row in argument \code{ymd} has \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar360-month_days}{}}} +\subsection{Method \code{month_days()}}{ +Determine the number of days in the month of the calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar360$month_days(ymd = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A vector indicating the number of days in each month for the +dates supplied as argument \code{ymd}. If no dates are supplied, the number +of days per month for the calendar as a vector of length 12. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar360-leap_year}{}}} +\subsection{Method \code{leap_year()}}{ +Indicate which years are leap years. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar360$leap_year(yr)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{yr}}{Integer vector of years to test.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{yr}. Since this +calendar does not use leap days, all values will be \code{FALSE}, or \code{NA} +where argument \code{yr} is \code{NA}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar360-date2offset}{}}} +\subsection{Method \code{date2offset()}}{ +Calculate difference in days between a \code{data.frame} of time +parts and the origin. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar360$date2offset(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{\code{data.frame}. Dates to calculate the difference for.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Integer vector of a length equal to the number of rows in +argument \code{x} indicating the number of days between \code{x} and the \code{origin}, +or \code{NA} for rows in \code{x} with \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar360-offset2date}{}}} +\subsection{Method \code{offset2date()}}{ +Calculate date parts from day differences from the origin. +This only deals with days as these are impacted by the calendar. +Hour-minute-second timestamp parts are handled in \link{CFCalendar}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar360$offset2date(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{Integer vector of days to add to the origin.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns 'year', 'month' and 'day' and as many +rows as the length of vector \code{x}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar360-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar360$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/CFCalendar365.Rd b/man/CFCalendar365.Rd new file mode 100644 index 0000000..8a84622 --- /dev/null +++ b/man/CFCalendar365.Rd @@ -0,0 +1,194 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CFCalendar365.R +\docType{class} +\name{CFCalendar365} +\alias{CFCalendar365} +\title{365-day CF calendar} +\description{ +This class represents a CF calendar of 365 days per year, having +no leap days in any year. This calendar is not compatible with the standard +POSIXt calendar. + +This calendar supports dates before year 1 and includes the year 0. +} +\section{Super class}{ +\code{\link[CFtime:CFCalendar]{CFtime::CFCalendar}} -> \code{CFCalendar365} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFCalendar365-new}{\code{CFCalendar365$new()}} +\item \href{#method-CFCalendar365-valid_days}{\code{CFCalendar365$valid_days()}} +\item \href{#method-CFCalendar365-month_days}{\code{CFCalendar365$month_days()}} +\item \href{#method-CFCalendar365-leap_year}{\code{CFCalendar365$leap_year()}} +\item \href{#method-CFCalendar365-date2offset}{\code{CFCalendar365$date2offset()}} +\item \href{#method-CFCalendar365-offset2date}{\code{CFCalendar365$offset2date()}} +\item \href{#method-CFCalendar365-clone}{\code{CFCalendar365$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar365-new}{}}} +\subsection{Method \code{new()}}{ +Create a new CF calendar of 365 days per year. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar365$new(nm, definition)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{nm}}{The name of the calendar. This must be "365_day" or "noleap".} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new instance of this class. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar365-valid_days}{}}} +\subsection{Method \code{valid_days()}}{ +Indicate which of the supplied dates are valid. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar365$valid_days(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{ymd} has rows +with \code{TRUE} for valid days and \code{FALSE} for invalid days, or \code{NA} where +the row in argument \code{ymd} has \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar365-month_days}{}}} +\subsection{Method \code{month_days()}}{ +Determine the number of days in the month of the calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar365$month_days(ymd = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame}, optional, with dates parsed into their parts.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A vector indicating the number of days in each month for the +dates supplied as argument \code{ymd}. If no dates are supplied, the number +of days per month for the calendar as a vector of length 12. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar365-leap_year}{}}} +\subsection{Method \code{leap_year()}}{ +Indicate which years are leap years. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar365$leap_year(yr)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{yr}}{Integer vector of years to test.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{yr}. Since this +calendar does not use leap days, all values will be \code{FALSE}, or \code{NA} +where argument \code{yr} is \code{NA}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar365-date2offset}{}}} +\subsection{Method \code{date2offset()}}{ +Calculate difference in days between a \code{data.frame} of time +parts and the origin. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar365$date2offset(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{\code{data.frame}. Dates to calculate the difference for.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Integer vector of a length equal to the number of rows in +argument \code{x} indicating the number of days between \code{x} and the \code{origin}, +or \code{NA} for rows in \code{x} with \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar365-offset2date}{}}} +\subsection{Method \code{offset2date()}}{ +Calculate date parts from day differences from the origin. This +only deals with days as these are impacted by the calendar. +Hour-minute-second timestamp parts are handled in \link{CFCalendar}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar365$offset2date(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{Integer vector of days to add to the origin.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns 'year', 'month' and 'day' and as many +rows as the length of vector \code{x}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar365-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar365$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/CFCalendar366.Rd b/man/CFCalendar366.Rd new file mode 100644 index 0000000..ea2880f --- /dev/null +++ b/man/CFCalendar366.Rd @@ -0,0 +1,194 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CFCalendar366.R +\docType{class} +\name{CFCalendar366} +\alias{CFCalendar366} +\title{366-day CF calendar} +\description{ +This class represents a CF calendar of 366 days per year, having +leap days in every year. This calendar is not compatible with the standard +POSIXt calendar. + +This calendar supports dates before year 1 and includes the year 0. +} +\section{Super class}{ +\code{\link[CFtime:CFCalendar]{CFtime::CFCalendar}} -> \code{CFCalendar366} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFCalendar366-new}{\code{CFCalendar366$new()}} +\item \href{#method-CFCalendar366-valid_days}{\code{CFCalendar366$valid_days()}} +\item \href{#method-CFCalendar366-month_days}{\code{CFCalendar366$month_days()}} +\item \href{#method-CFCalendar366-leap_year}{\code{CFCalendar366$leap_year()}} +\item \href{#method-CFCalendar366-date2offset}{\code{CFCalendar366$date2offset()}} +\item \href{#method-CFCalendar366-offset2date}{\code{CFCalendar366$offset2date()}} +\item \href{#method-CFCalendar366-clone}{\code{CFCalendar366$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar366-new}{}}} +\subsection{Method \code{new()}}{ +Create a new CF calendar of 366 days per year. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar366$new(nm, definition)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{nm}}{The name of the calendar. This must be "366_day" or "all_leap".} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new instance of this class. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar366-valid_days}{}}} +\subsection{Method \code{valid_days()}}{ +Indicate which of the supplied dates are valid. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar366$valid_days(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{ymd} has rows +with \code{TRUE} for valid days and \code{FALSE} for invalid days, or \code{NA} where +the row in argument \code{ymd} has \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar366-month_days}{}}} +\subsection{Method \code{month_days()}}{ +Determine the number of days in the month of the calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar366$month_days(ymd = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame}, optional, with dates parsed into their parts.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A vector indicating the number of days in each month for the +dates supplied as argument \code{ymd}. If no dates are supplied, the number +of days per month for the calendar as a vector of length 12. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar366-leap_year}{}}} +\subsection{Method \code{leap_year()}}{ +Indicate which years are leap years. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar366$leap_year(yr)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{yr}}{Integer vector of years to test.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{yr}. Since in +this calendar all years have a leap day, all values will be \code{TRUE}, or +\code{NA} where argument \code{yr} is \code{NA}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar366-date2offset}{}}} +\subsection{Method \code{date2offset()}}{ +Calculate difference in days between a \code{data.frame} of time +parts and the origin. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar366$date2offset(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{\code{data.frame}. Dates to calculate the difference for.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Integer vector of a length equal to the number of rows in +argument \code{x} indicating the number of days between \code{x} and the \code{origin}, +or \code{NA} for rows in \code{x} with \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar366-offset2date}{}}} +\subsection{Method \code{offset2date()}}{ +Calculate date parts from day differences from the origin. This +only deals with days as these are impacted by the calendar. +Hour-minute-second timestamp parts are handled in \link{CFCalendar}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar366$offset2date(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{Integer vector of days to add to the origin.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns 'year', 'month' and 'day' and as many +rows as the length of vector \code{x}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendar366-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendar366$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/CFCalendarJulian.Rd b/man/CFCalendarJulian.Rd new file mode 100644 index 0000000..d25edd3 --- /dev/null +++ b/man/CFCalendarJulian.Rd @@ -0,0 +1,198 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CFCalendarJulian.R +\docType{class} +\name{CFCalendarJulian} +\alias{CFCalendarJulian} +\title{Julian CF calendar} +\description{ +This class represents a Julian calendar of 365 days per year, +with every fourth year being a leap year of 366 days. The months and the +year align with the standard calendar. This calendar is not compatible with +the standard POSIXt calendar. + +This calendar starts on 1 January of year 1: 0001-01-01 00:00:00. Any dates +before this will generate an error. +} +\section{Super class}{ +\code{\link[CFtime:CFCalendar]{CFtime::CFCalendar}} -> \code{CFCalendarJulian} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFCalendarJulian-new}{\code{CFCalendarJulian$new()}} +\item \href{#method-CFCalendarJulian-valid_days}{\code{CFCalendarJulian$valid_days()}} +\item \href{#method-CFCalendarJulian-month_days}{\code{CFCalendarJulian$month_days()}} +\item \href{#method-CFCalendarJulian-leap_year}{\code{CFCalendarJulian$leap_year()}} +\item \href{#method-CFCalendarJulian-date2offset}{\code{CFCalendarJulian$date2offset()}} +\item \href{#method-CFCalendarJulian-offset2date}{\code{CFCalendarJulian$offset2date()}} +\item \href{#method-CFCalendarJulian-clone}{\code{CFCalendarJulian$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarJulian-new}{}}} +\subsection{Method \code{new()}}{ +Create a new CF calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarJulian$new(nm, definition)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{nm}}{The name of the calendar. This must be "julian". This argument +is superfluous but maintained to be consistent with the initialization +methods of the parent and sibling classes.} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new instance of this class. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarJulian-valid_days}{}}} +\subsection{Method \code{valid_days()}}{ +Indicate which of the supplied dates are valid. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarJulian$valid_days(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{ymd} has rows +with \code{TRUE} for valid days and \code{FALSE} for invalid days, or \code{NA} where +the row in argument \code{ymd} has \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarJulian-month_days}{}}} +\subsection{Method \code{month_days()}}{ +Determine the number of days in the month of the calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarJulian$month_days(ymd = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame}, optional, with dates parsed into their parts.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A vector indicating the number of days in each month for the +dates supplied as argument \code{ymd}. If no dates are supplied, the number +of days per month for the calendar as a vector of length 12, for a +regular year without a leap day. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarJulian-leap_year}{}}} +\subsection{Method \code{leap_year()}}{ +Indicate which years are leap years. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarJulian$leap_year(yr)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{yr}}{Integer vector of years to test.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{yr}. \code{NA} is +returned where elements in argument \code{yr} are \code{NA}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarJulian-date2offset}{}}} +\subsection{Method \code{date2offset()}}{ +Calculate difference in days between a \code{data.frame} of time +parts and the origin. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarJulian$date2offset(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{\code{data.frame}. Dates to calculate the difference for.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Integer vector of a length equal to the number of rows in +argument \code{x} indicating the number of days between \code{x} and the origin +of the calendar, or \code{NA} for rows in \code{x} with \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarJulian-offset2date}{}}} +\subsection{Method \code{offset2date()}}{ +Calculate date parts from day differences from the origin. This +only deals with days as these are impacted by the calendar. +Hour-minute-second timestamp parts are handled in \link{CFCalendar}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarJulian$offset2date(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{Integer vector of days to add to the origin.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns 'year', 'month' and 'day' and as many +rows as the length of vector \code{x}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarJulian-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarJulian$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/CFCalendarProleptic.Rd b/man/CFCalendarProleptic.Rd new file mode 100644 index 0000000..80a6da4 --- /dev/null +++ b/man/CFCalendarProleptic.Rd @@ -0,0 +1,221 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CFCalendarProleptic.R +\docType{class} +\name{CFCalendarProleptic} +\alias{CFCalendarProleptic} +\title{Proleptic Gregorian CF calendar} +\description{ +This class represents a standard CF calendar, but with the +Gregorian calendar extended backwards to before the introduction of the +Gregorian calendar. This calendar is compatible with the standard POSIXt +calendar, but note that daylight savings time is not considered. + +This calendar includes dates 1582-10-14 to 1582-10-05 (the gap between the +Gregorian and Julian calendars, which is observed by the standard +calendar), and extends to years before the year 1, including year 0. +} +\section{Super class}{ +\code{\link[CFtime:CFCalendar]{CFtime::CFCalendar}} -> \code{CFCalendarProleptic} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFCalendarProleptic-new}{\code{CFCalendarProleptic$new()}} +\item \href{#method-CFCalendarProleptic-valid_days}{\code{CFCalendarProleptic$valid_days()}} +\item \href{#method-CFCalendarProleptic-month_days}{\code{CFCalendarProleptic$month_days()}} +\item \href{#method-CFCalendarProleptic-leap_year}{\code{CFCalendarProleptic$leap_year()}} +\item \href{#method-CFCalendarProleptic-POSIX_compatible}{\code{CFCalendarProleptic$POSIX_compatible()}} +\item \href{#method-CFCalendarProleptic-date2offset}{\code{CFCalendarProleptic$date2offset()}} +\item \href{#method-CFCalendarProleptic-offset2date}{\code{CFCalendarProleptic$offset2date()}} +\item \href{#method-CFCalendarProleptic-clone}{\code{CFCalendarProleptic$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-new}{}}} +\subsection{Method \code{new()}}{ +Create a new CF calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$new(nm, definition)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{nm}}{The name of the calendar. This must be "proleptic_gregorian". +This argument is superfluous but maintained to be consistent with the +initialization methods of the parent and sibling classes.} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new instance of this class. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-valid_days}{}}} +\subsection{Method \code{valid_days()}}{ +Indicate which of the supplied dates are valid. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$valid_days(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{ymd} has rows +with \code{TRUE} for valid days and \code{FALSE} for invalid days, or \code{NA} where +the row in argument \code{ymd} has \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-month_days}{}}} +\subsection{Method \code{month_days()}}{ +Determine the number of days in the month of the calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$month_days(ymd = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame}, optional, with dates parsed into their parts.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Integer vector indicating the number of days in each month for +the dates supplied as argument \code{ymd}. If no dates are supplied, the +number of days per month for the calendar as a vector of length 12, for +a regular year without a leap day. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-leap_year}{}}} +\subsection{Method \code{leap_year()}}{ +Indicate which years are leap years. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$leap_year(yr)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{yr}}{Integer vector of years to test.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{yr}. \code{NA} is +returned where elements in argument \code{yr} are \code{NA}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-POSIX_compatible}{}}} +\subsection{Method \code{POSIX_compatible()}}{ +Indicate if the time series described using this calendar +can be safely converted to a standard date-time type (\code{POSIXct}, +\code{POSIXlt}, \code{Date}). +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$POSIX_compatible(offsets)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{offsets}}{The offsets from the CFtime instance.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{TRUE}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-date2offset}{}}} +\subsection{Method \code{date2offset()}}{ +Calculate difference in days between a \code{data.frame} of time +parts and the origin. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$date2offset(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{\code{data.frame}. Dates to calculate the difference for.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Integer vector of a length equal to the number of rows in +argument \code{x} indicating the number of days between \code{x} and the \code{origin}, +or \code{NA} for rows in \code{x} with \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-offset2date}{}}} +\subsection{Method \code{offset2date()}}{ +Calculate date parts from day differences from the origin. This +only deals with days as these are impacted by the calendar. +Hour-minute-second timestamp parts are handled in \link{CFCalendar}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$offset2date(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{Integer vector of days to add to the origin.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns 'year', 'month' and 'day' and as many +rows as the length of vector \code{x}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarProleptic-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarProleptic$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/CFCalendarStandard.Rd b/man/CFCalendarStandard.Rd new file mode 100644 index 0000000..49438b4 --- /dev/null +++ b/man/CFCalendarStandard.Rd @@ -0,0 +1,260 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CFCalendarStandard.R +\docType{class} +\name{CFCalendarStandard} +\alias{CFCalendarStandard} +\title{Standard CF calendar} +\description{ +This class represents a standard calendar of 365 or 366 days per +year. This calendar is compatible with the standard POSIXt calendar for +periods after the introduction of the Gregorian calendar, 1582-10-15 +00:00:00. The calendar starts at 0001-01-01 00:00:00, e.g. the start of the +Common Era. + +Note that this calendar, despite its name, is not the same as that used in +ISO8601 or many computer systems for periods prior to the introduction of +the Gregorian calendar. Use of the "proleptic_gregorian" calendar is +recommended for periods before or straddling the introduction date, as that +calendar is compatible with POSIXt on most OSes. +} +\section{Super class}{ +\code{\link[CFtime:CFCalendar]{CFtime::CFCalendar}} -> \code{CFCalendarStandard} +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{gap}}{The integer offset for 1582-10-15 00:00:00, when the Gregorian +calendar started, or 1582-10-05, when the gap between Julian and +Gregorian calendars started. The former is set when the calendar origin +is more recent, the latter when the origin is prior to the gap.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFCalendarStandard-new}{\code{CFCalendarStandard$new()}} +\item \href{#method-CFCalendarStandard-valid_days}{\code{CFCalendarStandard$valid_days()}} +\item \href{#method-CFCalendarStandard-is_gregorian_date}{\code{CFCalendarStandard$is_gregorian_date()}} +\item \href{#method-CFCalendarStandard-POSIX_compatible}{\code{CFCalendarStandard$POSIX_compatible()}} +\item \href{#method-CFCalendarStandard-month_days}{\code{CFCalendarStandard$month_days()}} +\item \href{#method-CFCalendarStandard-leap_year}{\code{CFCalendarStandard$leap_year()}} +\item \href{#method-CFCalendarStandard-date2offset}{\code{CFCalendarStandard$date2offset()}} +\item \href{#method-CFCalendarStandard-offset2date}{\code{CFCalendarStandard$offset2date()}} +\item \href{#method-CFCalendarStandard-clone}{\code{CFCalendarStandard$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-new}{}}} +\subsection{Method \code{new()}}{ +Create a new CF calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$new(nm, definition)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{nm}}{The name of the calendar. This must be "standard" or +"gregorian" (deprecated).} + +\item{\code{definition}}{The string that defines the units and the origin, as +per the CF Metadata Conventions.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new instance of this class. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-valid_days}{}}} +\subsection{Method \code{valid_days()}}{ +Indicate which of the supplied dates are valid. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$valid_days(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{ymd} has rows +with \code{TRUE} for valid days and \code{FALSE} for invalid days, or \code{NA} where +the row in argument \code{ymd} has \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-is_gregorian_date}{}}} +\subsection{Method \code{is_gregorian_date()}}{ +Indicate which of the supplied dates are in the Gregorian +part of the calendar, e.g. 1582-10-15 or after. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$is_gregorian_date(ymd)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame} with dates parsed into their parts in columns +\code{year}, \code{month} and \code{day}. Any other columns are disregarded.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{ymd} has rows +with \code{TRUE} for days in the Gregorian part of the calendar and \code{FALSE} +otherwise, or \code{NA} where the row in argument \code{ymd} has \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-POSIX_compatible}{}}} +\subsection{Method \code{POSIX_compatible()}}{ +Indicate if the time series described using this calendar +can be safely converted to a standard date-time type (\code{POSIXct}, +\code{POSIXlt}, \code{Date}). This is only the case if all offsets are for +timestamps fall on or after the start of the Gregorian calendar, +1582-10-15 00:00:00. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$POSIX_compatible(offsets)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{offsets}}{The offsets from the CFtime instance.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{TRUE}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-month_days}{}}} +\subsection{Method \code{month_days()}}{ +Determine the number of days in the month of the calendar. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$month_days(ymd = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ymd}}{\code{data.frame}, optional, with dates parsed into their parts.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A vector indicating the number of days in each month for the +dates supplied as argument \code{ymd}. If no dates are supplied, the number +of days per month for the calendar as a vector of length 12, for a +regular year without a leap day. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-leap_year}{}}} +\subsection{Method \code{leap_year()}}{ +Indicate which years are leap years. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$leap_year(yr)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{yr}}{Integer vector of years to test.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Logical vector with the same length as argument \code{yr}. \code{NA} is +returned where elements in argument \code{yr} are \code{NA}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-date2offset}{}}} +\subsection{Method \code{date2offset()}}{ +Calculate difference in days between a \code{data.frame} of time +parts and the origin. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$date2offset(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{\code{data.frame}. Dates to calculate the difference for.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Integer vector of a length equal to the number of rows in +argument \code{x} indicating the number of days between \code{x} and the origin +of the calendar, or \code{NA} for rows in \code{x} with \code{NA} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-offset2date}{}}} +\subsection{Method \code{offset2date()}}{ +Calculate date parts from day differences from the origin. This +only deals with days as these are impacted by the calendar. +Hour-minute-second timestamp parts are handled in \link{CFCalendar}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$offset2date(x)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{Integer vector of days to add to the origin.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{data.frame} with columns 'year', 'month' and 'day' and as many +rows as the length of vector \code{x}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFCalendarStandard-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFCalendarStandard$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/CFdatum-class.Rd b/man/CFdatum-class.Rd deleted file mode 100644 index 95e659c..0000000 --- a/man/CFdatum-class.Rd +++ /dev/null @@ -1,42 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFdatum.R -\docType{class} -\name{CFdatum-class} -\alias{CFdatum-class} -\title{CFdatum class} -\value{ -An object of class CFdatum -} -\description{ -This internal class stores the information to represent date and time values using -the CF conventions. This class is not supposed to be used by end-users directly. -An instance is created by the exported \code{CFtime} class, which also exposes the -relevant properties of this class. -} -\details{ -The following calendars are supported: - -\itemize{ -\item \code{gregorian} or \code{standard}, the international standard calendar for civil use. -\item \code{proleptic_gregorian}, the standard calendar but extending before 1582-10-15 -when the Gregorian calendar was adopted. -\item \code{noleap} or \verb{365_day}, all years have 365 days. -\item \code{all_leap} or \verb{366_day}, all years have 366 days. -\item \verb{360_day}, all years have 360 days, divided over 12 months of 30 days. -\item \code{julian}, every fourth year is a leap year (so including the years 1700, 1800, 1900, 2100, etc). -} -} -\section{Slots}{ - -\describe{ -\item{\code{definition}}{character. The string that defines the time unit and base date/time.} - -\item{\code{unit}}{numeric. The unit of time in which offsets are expressed.} - -\item{\code{origin}}{data.frame. Data frame with 1 row that defines the origin time.} - -\item{\code{calendar}}{character. The CF-calendar for the instance.} - -\item{\code{cal_id}}{numeric. The internal identifier of the CF-calendar to use.} -}} - diff --git a/man/CFdatum.Rd b/man/CFdatum.Rd deleted file mode 100644 index 5a08a11..0000000 --- a/man/CFdatum.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFdatum.R -\name{CFdatum} -\alias{CFdatum} -\title{Create a CFdatum object} -\usage{ -CFdatum(definition, calendar) -} -\arguments{ -\item{definition}{character. A character string describing the time coordinate -of a CF-compliant data file.} - -\item{calendar}{character. A character string describing the calendar to use -with the time dimension definition string.} -} -\value{ -An object of the \code{CFdatum} class. -} -\description{ -This function creates an instance of the \code{CFdatum} class. After creation the -instance is read-only. The parameters to the call are typically read from a -CF-compliant data file with climatological observations or predictions. -} diff --git a/man/CFfactor.Rd b/man/CFfactor.Rd index 84fd598..312fe9b 100644 --- a/man/CFfactor.Rd +++ b/man/CFfactor.Rd @@ -1,57 +1,58 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFfactor.R +% Please edit documentation in R/api.R \name{CFfactor} \alias{CFfactor} -\title{Create a factor from the offsets in an CFtime instance} +\title{Create a factor from the offsets in a \code{CFTime} instance} \usage{ -CFfactor(cf, period = "month", epoch = NULL) +CFfactor(t, period = "month", era = NULL) } \arguments{ -\item{cf}{CFtime. An instance of the \code{CFtime} class whose offsets will -be used to construct the factor.} +\item{t}{An instance of the \code{CFTime} class whose offsets will be used to +construct the factor.} -\item{period}{character. A character string with one of the values -"year", "season", "quarter", "month" (the default), "dekad" or "day".} +\item{period}{character. A character string with one of the values "year", +"season", "quarter", "month" (the default), "dekad" or "day".} -\item{epoch}{numeric or list, optional. Vector of years for which to +\item{era}{numeric or list, optional. Vector of years for which to construct the factor, or a list whose elements are each a vector of years. -If \code{epoch} is not specified, the factor will use the entire time series for +If \code{era} is not specified, the factor will use the entire time series for the factor.} } \value{ -If \code{epoch} is a single vector or not specified, a factor with a -length equal to the number of offsets in \code{cf}. If \code{epoch} is a list, a list -with the same number of elements and names as \code{epoch}, each containing a +If \code{era} is a single vector or not specified, a factor with a +length equal to the number of offsets in \code{t}. If \code{era} is a list, a list +with the same number of elements and names as \code{era}, each containing a factor. Elements in the factor will be set to \code{NA} for time series values outside of the range of specified years. -The factor, or factors in the list, have attributes 'period', 'epoch' and -'CFtime'. Attribute 'period' holds the value of the \code{period} argument. -Attribute 'epoch' indicates the number of years that are included in the -epoch, or -1 if no \code{epoch} is provided. Attribute 'CFtime' holds an -instance of CFtime that has the same definition as \code{cf}, but with offsets -corresponding to the mid-point of non-epoch factor levels; if the \code{epoch} -argument is specified, attribute 'CFtime' is \code{NULL}. +The factor, or factors in the list, have attributes 'period', 'era' and +'CFTime'. Attribute 'period' holds the value of the \code{period} argument. +Attribute 'era' indicates the number of years that are included in the +era, or -1 if no \code{era} is provided. Attribute 'CFTime' holds an +instance of \code{CFTime} that has the same definition as \code{t}, but with offsets +corresponding to the mid-point of non-era factor levels; if the \code{era} +argument is specified, attribute 'CFTime' is \code{NULL}. } \description{ With this function a factor can be generated for the time series, or a part -thereof, contained in the \code{CFtime} instance. This is specifically interesting +thereof, contained in the \link{CFTime} instance. This is specifically interesting for creating factors from the date part of the time series that aggregate the time series into longer time periods (such as month) that can then be used to process daily CF data sets using, for instance, \code{tapply()}. } \details{ -The factor will respect the calendar of the datum that the time series is -built on. For \code{period}s longer than a day this will result in a factor where -the calendar is no longer relevant (because calendars impacts days, not -dekads, months, quarters, seasons or years). +The factor will respect the calendar that the time series is built on. For +\code{period}s longer than a day this will result in a factor where the calendar +is no longer relevant (because calendars impacts days, not dekads, months, +quarters, seasons or years). -The factor will be generated in the order of the offsets of the \code{CFtime} +The factor will be generated in the order of the offsets of the \code{CFTime} instance. While typical CF-compliant data sources use ordered time series there is, however, no guarantee that the factor is ordered as multiple -\code{CFtime} objects may have been merged out of order. +\code{CFTime} objects may have been merged out of order. For most processing with +a factor the ordering is of no concern. -If the \code{epoch} parameter is specified, either as a vector of years to include +If the \code{era} parameter is specified, either as a vector of years to include in the factor, or as a list of such vectors, the factor will only consider those values in the time series that fall within the list of years, inclusive of boundary values. Other values in the factor will be set to \code{NA}. The years @@ -63,7 +64,7 @@ The following periods are supported by this function: \itemize{ \item \code{year}, the year of each offset is returned as "YYYY". \item \code{season}, the meteorological season of each offset is returned as -"Sx", with x being 1-4, preceeded by "YYYY" if no \code{epoch} is +"Sx", with x being 1-4, preceeded by "YYYY" if no \code{era} is specified. Note that December dates are labeled as belonging to the subsequent year, so the date "2020-12-01" yields "2021S1". This implies that for standard CMIP files having one or more full years of data the @@ -71,20 +72,20 @@ first season will have data for the first two months (January and February), while the final season will have only a single month of data (December). \item \code{quarter}, the calendar quarter of each offset is returned as "Qx", -with x being 1-4, preceeded by "YYYY" if no \code{epoch} is specified. +with x being 1-4, preceeded by "YYYY" if no \code{era} is specified. \item \code{month}, the month of each offset is returned as "01" to -"12", preceeded by "YYYY-" if no \code{epoch} is specified. This is the default +"12", preceeded by "YYYY-" if no \code{era} is specified. This is the default period. \item \code{dekad}, ten-day periods are returned as -"Dxx", where xx runs from "01" to "36", preceeded by "YYYY" if no \code{epoch} +"Dxx", where xx runs from "01" to "36", preceeded by "YYYY" if no \code{era} is specified. Each month is subdivided in dekads as follows: 1- days 01 - 10; 2- days 11 - 20; 3- remainder of the month. \item \code{day}, the month and day of each offset are returned as "MM-DD", -preceeded by "YYYY-" if no \code{epoch} is specified. +preceeded by "YYYY-" if no \code{era} is specified. } It is not possible to create a factor for a period that is shorter than the -temporal resolution of the source data set from which the \code{cf} argument +temporal resolution of the source data set from which the \code{t} argument derives. As an example, if the source data set has monthly data, a dekad or day factor cannot be created. @@ -92,23 +93,24 @@ Creating factors for other periods is not supported by this function. Factors based on the timestamp information and not dependent on the calendar can trivially be constructed from the output of the \code{\link[=as_timestamp]{as_timestamp()}} function. -For non-epoch factors the attribute 'CFtime' of the result contains a CFtime +For non-era factors the attribute 'CFTime' of the result contains a \code{CFTime} instance that is valid for the result of applying the factor to a data set -that the \code{cf} argument is associated with. In other words, if CFtime instance -'Acf' describes the temporal dimension of data set 'A' and a factor 'Af' is -generated from 'Acf', then \code{attr(Af, "CFtime")} describes the temporal -dimension of the result of, say, \code{apply(A, 1:2, tapply, Af, FUN)}. The -'CFtime' attribute is \code{NULL} for epoch factors. +that the \code{t} argument is associated with. In other words, if \code{CFTime} +instance 'At' describes the temporal dimension of data set 'A' and a factor +'Af' is generated like \code{Af <- CFfactor(At)}, then \code{Bt <- attr(Af, "CFTime")} +describes the temporal dimension of the result of, say, +\code{B <- apply(A, 1:2, tapply, Af, FUN)}. The 'CFTime' attribute is \code{NULL} for +era factors. } \examples{ -cf <- CFtime("days since 1949-12-01", "360_day", 19830:54029) +t <- CFtime("days since 1949-12-01", "360_day", 19830:54029) # Create a dekad factor for the whole time series -f <- CFfactor(cf, "dekad") +f <- CFfactor(t, "dekad") -# Create three monthly factors for early, mid and late 21st century epochs -ep <- CFfactor(cf, epoch = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) +# Create three monthly factors for early, mid and late 21st century eras +ep <- CFfactor(t, era = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) } \seealso{ -\code{\link[=cut]{cut()}} creates a non-epoch factor for arbitrary cut points. +\code{\link[=cut]{cut()}} creates a non-era factor for arbitrary cut points. } diff --git a/man/CFfactor_coverage.Rd b/man/CFfactor_coverage.Rd index f21ff31..0b195ee 100644 --- a/man/CFfactor_coverage.Rd +++ b/man/CFfactor_coverage.Rd @@ -1,16 +1,16 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFfactor.R +% Please edit documentation in R/api.R \name{CFfactor_coverage} \alias{CFfactor_coverage} \title{Coverage of time elements for each factor level} \usage{ -CFfactor_coverage(cf, f, coverage = "absolute") +CFfactor_coverage(t, f, coverage = "absolute") } \arguments{ -\item{cf}{CFtime. An instance of CFtime.} +\item{t}{An instance of \link{CFTime}.} \item{f}{factor or list. A factor or a list of factors derived from the -parameter \code{cf}. The factor or list thereof should generally be generated by +parameter \code{t}. The factor or list thereof should generally be generated by the function \code{\link[=CFfactor]{CFfactor()}}.} \item{coverage}{"absolute" or "relative".} @@ -18,7 +18,7 @@ the function \code{\link[=CFfactor]{CFfactor()}}.} \value{ If \code{f} is a factor, a numeric vector with a length equal to the number of levels in the factor, indicating the number of units from the -time series in \code{cf} contained in each level of the factor when +time series in \code{t} contained in each level of the factor when \code{coverage = "absolute"} or the proportion of units present relative to the maximum number when \code{coverage = "relative"}. If \code{f} is a list of factors, a list with each element a numeric vector as above. @@ -28,7 +28,7 @@ This function calculates the number of time elements, or the relative coverage, in each level of a factor generated by \code{\link[=CFfactor]{CFfactor()}}. } \examples{ -cf <- CFtime("days since 2001-01-01", "365_day", 0:364) -f <- CFfactor(cf, "dekad") -CFfactor_coverage(cf, f, "absolute") +t <- CFtime("days since 2001-01-01", "365_day", 0:364) +f <- CFfactor(t, "dekad") +CFfactor_coverage(t, f, "absolute") } diff --git a/man/CFfactor_units.Rd b/man/CFfactor_units.Rd index 8a70ca4..0b78042 100644 --- a/man/CFfactor_units.Rd +++ b/man/CFfactor_units.Rd @@ -1,16 +1,16 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFfactor.R +% Please edit documentation in R/api.R \name{CFfactor_units} \alias{CFfactor_units} \title{Number of base time units in each factor level} \usage{ -CFfactor_units(cf, f) +CFfactor_units(t, f) } \arguments{ -\item{cf}{CFtime. An instance of CFtime.} +\item{t}{An instance of \code{CFTime}.} -\item{f}{factor or list. A factor or a list of factors derived from the -parameter \code{cf}. The factor or list thereof should generally be generated by +\item{f}{A factor or a list of factors derived from the +parameter \code{t}. The factor or list thereof should generally be generated by the function \code{\link[=CFfactor]{CFfactor()}}.} } \value{ @@ -20,7 +20,7 @@ level of the factor. If \code{f} is a list of factors, a list with each element a numeric vector as above. } \description{ -Given a factor as returned by \code{\link[=CFfactor]{CFfactor()}} and the \code{CFtime} instance from +Given a factor as returned by \code{\link[=CFfactor]{CFfactor()}} and the \link{CFTime} instance from which the factor was derived, this function will return a numeric vector with the number of time units in each level of the factor. } @@ -33,10 +33,10 @@ Going from average values back to absolute values for an aggregate period is easily done with the result of this function, without having to consider the specifics of the calendar of the data set. -If the factor \code{f} is for an epoch (e.g. spanning multiple years and the +If the factor \code{f} is for an era (e.g. spanning multiple years and the levels do not indicate the specific year), then the result will indicate the number of time units of the period in a regular single year. In other words, -for an epoch of 2041-2060 and a monthly factor on a standard calendar with a +for an era of 2041-2060 and a monthly factor on a standard calendar with a \code{days} unit, the result will be \code{c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)}. Leap days are thus only considered for the \verb{366_day} and \code{all_leap} calendars. @@ -47,7 +47,7 @@ number of data points or the coverage of data points relative to the factor level. } \examples{ -cf <- CFtime("days since 2001-01-01", "365_day", 0:364) -f <- CFfactor(cf, "dekad") -CFfactor_units(cf, f) +t <- CFtime("days since 2001-01-01", "365_day", 0:364) +f <- CFfactor(t, "dekad") +CFfactor_units(t, f) } diff --git a/man/CFtime-class.Rd b/man/CFtime-class.Rd deleted file mode 100644 index 9e46711..0000000 --- a/man/CFtime-class.Rd +++ /dev/null @@ -1,29 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\docType{class} -\name{CFtime-class} -\alias{CFtime-class} -\title{CF Metadata Conventions time representation} -\value{ -An object of class CFtime. -} -\description{ -CF Metadata Conventions time representation -} -\section{Slots}{ - -\describe{ -\item{\code{datum}}{CFdatum. The origin upon which the \code{offsets} are based.} - -\item{\code{resolution}}{numeric. The average number of time units between offsets.} - -\item{\code{offsets}}{numeric. A vector of offsets from the datum.} - -\item{\code{bounds}}{Optional, the bounds for the offsets. If not set, it is the -logical value \code{FALSE}. If set, it is the logical value \code{TRUE} if the bounds -are regular with respect to the regularly spaced offsets (e.g. successive -bounds are contiguous and at mid-points between the offsets); otherwise a -\code{matrix} with columns for \code{offsets} and low values in the first row, high -values in the second row.} -}} - diff --git a/man/CFtime-function.Rd b/man/CFtime-function.Rd new file mode 100644 index 0000000..c32d54c --- /dev/null +++ b/man/CFtime-function.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/api.R +\name{CFtime-function} +\alias{CFtime-function} +\alias{CFtime} +\title{Create a CFTime object} +\usage{ +CFtime(definition, calendar = "standard", offsets = NULL) +} +\arguments{ +\item{definition}{A character string describing the time coordinate.} + +\item{calendar}{A character string describing the calendar to use with the +time dimension definition string. Default value is "standard".} + +\item{offsets}{Numeric or character vector, optional. When numeric, a vector +of offsets from the origin in the time series. When a character vector of +length 2 or more, timestamps in ISO8601 or UDUNITS format. When a character +string, a timestamp in ISO8601 or UDUNITS format and then a time series +will be generated with a separation between steps equal to the unit of +measure in the definition, inclusive of the definition timestamp. The unit +of measure of the offsets is defined by the time series definition.} +} +\value{ +An instance of the \code{CFTime} class. +} +\description{ +This function creates an instance of the \link{CFTime} class. The arguments to the +call are typically read from a CF-compliant data file with climatological +observations or climate projections. Specification of arguments can also be +made manually in a variety of combinations. +} +\examples{ +CFtime("days since 1850-01-01", "julian", 0:364) + +CFtime("hours since 2023-01-01", "360_day", "2023-01-30T23:00") +} diff --git a/man/CFtime-package.Rd b/man/CFtime-package.Rd index 533256d..54e04da 100644 --- a/man/CFtime-package.Rd +++ b/man/CFtime-package.Rd @@ -16,25 +16,30 @@ POSIXt). The CF time coordinate is formally defined in the \href{https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#time-coordinate}{CF Metadata Conventions document}. } \details{ -The package can create a \code{CFtime} instance from scratch or, more commonly, it -can use the dimension attributes and dimension variable values from a NetCDF +The package can create a \link{CFTime} instance from scratch or, more commonly, it +can use the dimension attributes and dimension variable values from a netCDF resource. The package does not actually do any of the reading and the user is -free to use their NetCDF package of preference (with the two main options -being \href{https://cran.r-project.org/package=RNetCDF}{RNetCDF} and +free to use their netCDF package of preference. The recommended package to +use (with any netCDF resources) is \href{https://cran.r-project.org/package=ncdfCF}{ncdfCF}. +\code{ncdfCF} will automatically use this package to manage the "time" dimension +of any netCDF resource. As with this package, it reads and interprets the +attributes of the resource to apply the CF Metadata Conventions, supporting +axes, auxiliary coordinate variables, coordinate reference systems, etc. +Alternatively, for more basic netCDF reading and writing, the two main options +are \href{https://cran.r-project.org/package=RNetCDF}{RNetCDF} and \href{https://cran.r-project.org/package=ncdf4}{ncdf4}). \strong{Create, modify, inquire} \itemize{ -\item \code{\link[=CFtime]{CFtime()}}: Create a CFtime instance -\item \code{\link[=properties]{Properties}} of the CFtime instance -\item \code{\link[=CFparse]{CFparse()}}: Parse a vector of character timestamps into CFtime elements -\item \code{\link[=CFtime-equivalent]{Compare}} two CFtime instances -\item \code{\link[=CFtime-merge]{Merge}} two CFtime instances -\item \code{\link[=CFtime-append]{Append}} additional time steps to a CFtime instance -\item \code{\link[=as_timestamp]{as_timestamp()}} and \code{\link[=format]{format()}}: Generate a vector of character or \code{POSIXct} timestamps from a CFtime instance +\item \code{\link[=CFtime]{CFtime()}}: Create a \link{CFTime} instance +\item \code{\link[=properties]{Properties}} of the \code{CFTime} instance +\item \code{\link[=parse_timestamps]{parse_timestamps()}}: Parse a vector of character timestamps into \code{CFTime} elements +\item \code{\link[=CFtime-equivalent]{Compare}} two \code{CFTime} instances +\item \code{\link[=CFtime-merge]{Merge}} two \code{CFTime} instances or append additional time steps to a \code{CFTime} instance +\item \code{\link[=as_timestamp]{as_timestamp()}} and \code{\link[=format]{format()}}: Generate a vector of character or \code{POSIXct} timestamps from a \code{CFTime} instance \item \code{\link[=range]{range()}}: Timestamps of the two endpoints in the time series -\item \code{\link[=is_complete]{is_complete()}}: Does the CFtime instance have a complete time series between endpoints? -\item \code{\link[=month_days]{month_days()}}: How many days are there in a month using the CFtime calendar? +\item \code{\link[=is_complete]{is_complete()}}: Does the \code{CFTime} instance have a complete time series between endpoints? +\item \code{\link[=month_days]{month_days()}}: How many days are there in a month using the calendar of the \code{CFTime} instance? } \strong{Factors and coverage} diff --git a/man/CFtime.Rd b/man/CFtime.Rd index 5ff7d21..7e98469 100644 --- a/man/CFtime.Rd +++ b/man/CFtime.Rd @@ -1,37 +1,654 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/CFtime.R -\name{CFtime} -\alias{CFtime} -\title{Create a CFtime object} -\usage{ -CFtime(definition, calendar = "standard", offsets = NULL) -} -\arguments{ -\item{definition}{character. A character string describing the time coordinate -of a CF-compliant data file.} - -\item{calendar}{character. A character string describing the calendar to use -with the time dimension definition string. Default value is "standard".} - -\item{offsets}{numeric or character, optional. When numeric, a vector of -offsets from the origin in the time series. When a character vector, -timestamps in ISO8601 or UDUNITS format. When a character string, a -timestamp in ISO8601 or UDUNITS format and then a time series will be -generated with a separation between steps equal to the unit of measure in -the definition, inclusive of the definition timestamp. The unit of measure -of the offsets is defined by the time series definition.} -} -\value{ -An instance of the \code{CFtime} class. -} +\docType{class} +\name{CFTime} +\alias{CFTime} +\title{CFTime class} \description{ -This function creates an instance of the \code{CFtime} class. The arguments to -the call are typically read from a CF-compliant data file with climatological -observations or climate projections. Specification of arguments can also be -made manually in a variety of combinations. +This class manages the "time" dimension of netCDF files that +follow the CF Metadata Conventions, and its productive use in R. + +The class has a field \code{cal} which holds a specific calendar from the +allowed types (9 named calendars are currently supported). The calendar is +also implemented as a (hidden) class which converts netCDF file encodings to +timestamps as character strings, and vice-versa. Bounds information (the +period of time over which a timestamp is valid) is used when defined in the +netCDF file. + +Additionally, this class has functions to ease use of the netCDF "time" +information when processing data from netCDF files. Filtering and indexing of +time values is supported, as is the generation of factors. +} +\references{ +https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#time-coordinate +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{cal}}{The calendar of this \code{CFTime} instance, a descendant of the +\link{CFCalendar} class.} + +\item{\code{offsets}}{A numeric vector of offsets from the origin of the +calendar.} + +\item{\code{resolution}}{The average number of time units between offsets.} + +\item{\code{bounds}}{Optional, the bounds for the offsets. If not set, it is the +logical value \code{FALSE}. If set, it is the logical value \code{TRUE} if the +bounds are regular with respect to the regularly spaced offsets (e.g. +successive bounds are contiguous and at mid-points between the +offsets); otherwise a \code{matrix} with columns for \code{offsets} and low +values in the first row, high values in the second row.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-CFTime-new}{\code{CFTime$new()}} +\item \href{#method-CFTime-print}{\code{CFTime$print()}} +\item \href{#method-CFTime-range}{\code{CFTime$range()}} +\item \href{#method-CFTime-as_timestamp}{\code{CFTime$as_timestamp()}} +\item \href{#method-CFTime-format}{\code{CFTime$format()}} +\item \href{#method-CFTime-indexOf}{\code{CFTime$indexOf()}} +\item \href{#method-CFTime-get_bounds}{\code{CFTime$get_bounds()}} +\item \href{#method-CFTime-set_bounds}{\code{CFTime$set_bounds()}} +\item \href{#method-CFTime-equidistant}{\code{CFTime$equidistant()}} +\item \href{#method-CFTime-slab}{\code{CFTime$slab()}} +\item \href{#method-CFTime-POSIX_compatible}{\code{CFTime$POSIX_compatible()}} +\item \href{#method-CFTime-cut}{\code{CFTime$cut()}} +\item \href{#method-CFTime-factor}{\code{CFTime$factor()}} +\item \href{#method-CFTime-factor_units}{\code{CFTime$factor_units()}} +\item \href{#method-CFTime-factor_coverage}{\code{CFTime$factor_coverage()}} +\item \href{#method-CFTime-clone}{\code{CFTime$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-new}{}}} +\subsection{Method \code{new()}}{ +Create a new instance of this class. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$new(definition, calendar, offsets)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{definition}}{Character string of the units and origin of the +calendar.} + +\item{\code{calendar}}{Character string of the calendar to use. Must be one of +the values permitted by the CF Metadata Conventions. If \code{NULL}, the +"standard" calendar will be used.} + +\item{\code{offsets}}{Numeric or character vector, optional. When numeric, a +vector of offsets from the origin in the time series. When a character +vector of length 2 or more, timestamps in ISO8601 or UDUNITS format. +When a character string, a timestamp in ISO8601 or UDUNITS format and +then a time series will be generated with a separation between steps +equal to the unit of measure in the definition, inclusive of the +definition timestamp. The unit of measure of the offsets is defined by +the \code{definition} argument.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-print}{}}} +\subsection{Method \code{print()}}{ +Print a summary of the \code{CFTime} object to the console. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$print(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{Ignored.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{self} invisibly. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-range}{}}} +\subsection{Method \code{range()}}{ +This method returns the first and last timestamp of the time +series as a vector. Note that the offsets do not have to be sorted. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$range(format = "", bounds = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{format}}{Value of "date" or "timestamp". Optionally, a +character string that specifies an alternate format.} + +\item{\code{bounds}}{Logical to indicate if the extremes from the bounds should +be used, if set. Defaults to \code{FALSE}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Vector of two character strings that represent the starting and +ending timestamps in the time series. If a \code{format} is supplied, that +format will be used. Otherwise, if all of the timestamps in the time +series have a time component of \code{00:00:00} the date of the timestamp is +returned, otherwise the full timestamp (without any time zone +information). +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-as_timestamp}{}}} +\subsection{Method \code{as_timestamp()}}{ +This method generates a vector of character strings or +\code{POSIXct}s that represent the date and time in a selectable combination +for each offset. + +The character strings use the format \verb{YYYY-MM-DDThh:mm:ss±hhmm}, +depending on the \code{format} specifier. The date in the string is not +necessarily compatible with \code{POSIXt} - in the \verb{360_day} calendar +\code{2017-02-30} is valid and \code{2017-03-31} is not. + +For the "proleptic_gregorian" calendar the output can also be generated +as a vector of \code{POSIXct} values by specifying \code{asPOSIX = TRUE}. The +same is possible for the "standard" and "gregorian" calendars but only +if all timestamps fall on or after 1582-10-15. If \code{asPOSIX = TRUE} is +specified while the calendar does not support it, an error will be +generated. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$as_timestamp(format = NULL, asPOSIX = FALSE)}\if{html}{\out{
}} } -\examples{ -CFtime("days since 1850-01-01", "julian", 0:364) -CFtime("hours since 2023-01-01", "360_day", "2023-01-30T23:00") +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{format}}{character. A character string with either of the values +"date" or "timestamp". If the argument is not specified, the format +used is "timestamp" if there is time information, "date" otherwise.} + +\item{\code{asPOSIX}}{logical. If \code{TRUE}, for "standard", "gregorian" and +"proleptic_gregorian" calendars the output is a vector of \code{POSIXct} - +for other calendars an error will be thrown. Default value is \code{FALSE}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A character vector where each element represents a moment in +time according to the \code{format} specifier. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-format}{}}} +\subsection{Method \code{format()}}{ +Format timestamps using a specific format string, using the +specifiers defined for the \code{\link[base:strptime]{base::strptime()}} function, with +limitations. The only supported specifiers are \verb{bBdeFhHImMpRSTYz\%}. +Modifiers \code{E} and \code{O} are silently ignored. Other specifiers, including +their percent sign, are copied to the output as if they were adorning +text. + +The formatting is largely oblivious to locale. The reason for this is +that certain dates in certain calendars are not POSIX-compliant and the +system functions necessary for locale information thus do not work +consistently. The main exception to this is the (abbreviated) names of +months (\code{bB}), which could be useful for pretty printing in the local +language. For separators and other locale-specific adornments, use +local knowledge instead of depending on system locale settings; e.g. +specify \verb{\%m/\%d/\%Y} instead of \verb{\%D}. + +Week information, including weekday names, is not supported at all as a +"week" is not defined for non-standard CF calendars and not generally +useful for climate projection data. If you are working with observed +data and want to get pretty week formats, use the \code{\link[=as_timestamp]{as_timestamp()}} +method to generate \code{POSIXct} timestamps (observed data generally uses a +"standard" calendar) and then use the \code{\link[base:format]{base::format()}} function which +supports the full set of specifiers. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$format(format)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{format}}{A character string with \code{strptime} format specifiers. If +omitted, the most economical format will be used: a full timestamp when +time information is available, a date otherwise.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A vector of character strings with a properly formatted +timestamp. Any format specifiers not recognized or supported will be +returned verbatim. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-indexOf}{}}} +\subsection{Method \code{indexOf()}}{ +Find the index in the time series for each timestamp given +in argument \code{x}. Values of \code{x} that are before the earliest value in +the time series will be returned as \code{0}; values of \code{x} that are after +the latest values in the time series will be returned as +\code{.Machine$integer.max}. Alternatively, when \code{x} is a numeric vector of +index values, return the valid indices of the same vector, with the +side effect being the attribute "CFTime" associated with the result. + +Matching also returns index values for timestamps that fall between two +elements of the time series - this can lead to surprising results when +time series elements are positioned in the middle of an interval (as +the CF Metadata Conventions instruct us to "reasonably assume"): a time +series of days in January would be encoded in a netCDF file as +\code{c("2024-01-01 12:00:00", "2024-01-02 12:00:00", "2024-01-03 12:00:00", ...)} +so \code{x <- c("2024-01-01", "2024-01-02", "2024-01-03")} would +result in \verb{(NA, 1, 2)} (or \verb{(NA, 1.5, 2.5)} with \code{method = "linear"}) +because the date values in \code{x} are at midnight. This situation is +easily avoided by ensuring that this \code{CFTime} instance has bounds set +(use \code{bounds(y) <- TRUE} as a proximate solution if bounds are not +stored in the netCDF file). See the Examples. + +If bounds are set, the indices are taken from those bounds. Returned +indices may fall in between bounds if the latter are not contiguous, +with the exception of the extreme values in \code{x}. + +Values of \code{x} that are not valid timestamps according to the calendar +of this \code{CFTime} instance will be returned as \code{NA}. + +\code{x} can also be a numeric vector of index values, in which case the +valid values in \code{x} are returned. If negative values are passed, the +positive counterparts will be excluded and then the remainder returned. +Positive and negative values may not be mixed. Using a numeric vector +has the side effect that the result has the attribute "CFTime" +describing the temporal dimension of the slice. If index values outside +of the range of \code{self} are provided, an error will be thrown. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$indexOf(x, method = "constant")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x}}{Vector of character, POSIXt or Date values to find indices for, +or a numeric vector.} + +\item{\code{method}}{Single value of "constant" or "linear". If \code{"constant"} or +when bounds are set on \code{self}, return the index value for each +match. If \code{"linear"}, return the index value with any fractional value.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A numeric vector giving indices into the "time" dimension of the +dataset associated with \code{self} for the values of \code{x}. If there is at +least 1 valid index, then attribute "CFTime" contains an instance of +\code{CFTime} that describes the dimension of filtering the dataset +associated with \code{self} with the result of this function, excluding any +\code{NA}, \code{0} and \code{.Machine$integer.max} values. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-get_bounds}{}}} +\subsection{Method \code{get_bounds()}}{ +Return bounds. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$get_bounds(format)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{format}}{A string specifying a format for output, optional.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +An array with dims(2, length(offsets)) with values for the +bounds. \code{NULL} if the bounds have not been set. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-set_bounds}{}}} +\subsection{Method \code{set_bounds()}}{ +Set the bounds of the \code{CFTime} instance. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$set_bounds(value)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{value}}{The bounds to set, in units of the offsets. Either a matrix +\verb{(2, length(self$offsets))} or a logical.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{self} invisibly. +This method returns \code{TRUE} if the time series has uniformly distributed +time steps between the extreme values, \code{FALSE} otherwise. First test +without sorting; this should work for most data sets. If not, only then +offsets are sorted. For most data sets that will work but for implied +resolutions of month, season, year, etc based on a "days" or finer +calendar unit this will fail due to the fact that those coarser units +have a variable number of days per time step, in all calendars except for +\verb{360_day}. For now, an approximate solution is used that should work in +all but the most non-conformal exotic arrangements. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-equidistant}{}}} +\subsection{Method \code{equidistant()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$equidistant()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +\code{TRUE} if all time steps are equidistant, \code{FALSE} otherwise, or +\code{NA} if no offsets have been set. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-slab}{}}} +\subsection{Method \code{slab()}}{ +Given two extreme character timestamps, return a logical vector of a length +equal to the number of time steps in the time series with values \code{TRUE} +for those time steps that fall between the two extreme values, \code{FALSE} +otherwise. + +\strong{NOTE} Giving crap as the earlier timestamp will set that value to 0. So +invalid input will still generate a result. To be addressed. Crap in later +timestamp is not tolerated. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$slab(extremes, closed)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{extremes}}{Character vector of two timestamps that represent the +extremes of the time period of interest.} + +\item{\code{closed}}{Is the right side closed, i.e. included in the result?} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A logical vector with a length equal to the number of time steps in +\code{self} with values \code{TRUE} for those time steps that fall between the two +extreme values, \code{FALSE} otherwise. The earlier timestamp is included, the +later timestamp is excluded. A specification of \verb{c("2022-01-01", "2023-01-01)} +will thus include all time steps that fall in the year 2022. + +An attribute 'CFTime' will have the same definition as \code{self} but with offsets +corresponding to the time steps falling between the two extremes. If there +are no values between the extremes, the attribute is \code{NULL}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-POSIX_compatible}{}}} +\subsection{Method \code{POSIX_compatible()}}{ +Can the time series be converted to POSIXt? +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$POSIX_compatible()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +\code{TRUE} if the calendar support coversion to POSIXt, \code{FALSE} +otherwise. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-cut}{}}} +\subsection{Method \code{cut()}}{ +Create a factor for a \code{CFTime} instance. + +When argument \code{breaks} is one of \verb{"year", "season", "quarter", "month", "dekad", "day"}, a factor is generated like by \code{\link[=CFfactor]{CFfactor()}}. When +\code{breaks} is a vector of character timestamps a factor is produced with +a level for every interval between timestamps. The last timestamp, +therefore, is only used to close the interval started by the +pen-ultimate timestamp - use a distant timestamp (e.g. \code{range(x)[2]}) +to ensure that all offsets to the end of the CFTime time series are +included, if so desired. The last timestamp will become the upper bound +in the \code{CFTime} instance that is returned as an attribute to this +function so a sensible value for the last timestamp is advisable. + +This method works similar to \code{\link[base:cut.POSIXt]{base::cut.POSIXt()}} but there are some +differences in the arguments: for \code{breaks} the set of options is +different and no preceding integer is allowed, \code{labels} are always +assigned using values of \code{breaks}, and the interval is always +left-closed. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$cut(breaks)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{breaks}}{A character string of a factor period (see \code{\link[=CFfactor]{CFfactor()}} for +a description), or a character vector of timestamps that conform to the +calendar of \code{x}, with a length of at least 2. Timestamps must be given +in ISO8601 format, e.g. "2024-04-10 21:31:43".} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A factor with levels according to the \code{breaks} argument, with +attributes 'period', 'era' and 'CFTime'. When \code{breaks} is a factor +period, attribute 'period' has that value, otherwise it is '"day"'. +When \code{breaks} is a character vector of timestamps, attribute 'CFTime' +holds an instance of \code{CFTime} that has the same definition as \code{x}, but +with (ordered) offsets generated from the \code{breaks}. Attribute 'era' +is always -1. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-factor}{}}} +\subsection{Method \code{factor()}}{ +Generate a factor for the offsets, or a part thereof. This is +specifically interesting for creating factors from the date part of the +time series that aggregate the time series into longer time periods (such +as month) that can then be used to process daily CF data sets using, for +instance, \code{tapply()}. + +The factor will respect the calendar that the time series is built on. + +The factor will be generated in the order of the offsets. While typical +CF-compliant data sources use ordered time series there is, however, no +guarantee that the factor is ordered. For most processing with a factor +the ordering is of no concern. + +If the \code{era} parameter is specified, either as a vector of years to +include in the factor, or as a list of such vectors, the factor will only +consider those values in the time series that fall within the list of +years, inclusive of boundary values. Other values in the factor will be +set to \code{NA}. The years need not be contiguous, within a single vector or +among the list items, or in order. + +The following periods are supported by this method: + +\itemize{ +\item \code{year}, the year of each offset is returned as "YYYY". +\item \code{season}, the meteorological season of each offset is returned as +"Sx", with x being 1-4, preceeded by "YYYY" if no \code{era} is +specified. Note that December dates are labeled as belonging to the +subsequent year, so the date "2020-12-01" yields "2021S1". This implies +that for standard CMIP files having one or more full years of data the +first season will have data for the first two months (January and +February), while the final season will have only a single month of data +(December). +\item \code{quarter}, the calendar quarter of each offset is returned as "Qx", +with x being 1-4, preceeded by "YYYY" if no \code{era} is specified. +\item \code{month}, the month of each offset is returned as "01" to +"12", preceeded by "YYYY-" if no \code{era} is specified. This is the default +period. +\item \code{dekad}, ten-day periods are returned as +"Dxx", where xx runs from "01" to "36", preceeded by "YYYY" if no \code{era} +is specified. Each month is subdivided in dekads as follows: 1- days 01 - +10; 2- days 11 - 20; 3- remainder of the month. +\item \code{day}, the month and day of each offset are returned as "MM-DD", +preceeded by "YYYY-" if no \code{era} is specified. +} + +It is not possible to create a factor for a period that is shorter than +the temporal resolution of the calendar. As an example, if the calendar +has a monthly unit, a dekad or day factor cannot be created. + +Creating factors for other periods is not supported by this method. +Factors based on the timestamp information and not dependent on the +calendar can trivially be constructed from the output of the +\code{\link[=as_timestamp]{as_timestamp()}} function. + +For non-era factors the attribute 'CFTime' of the result contains a +\code{CFTime} instance that is valid for the result of applying the factor to +a resource that this instance is associated with. In other words, if +\code{CFTime} instance 'At' describes the temporal dimension of resource 'A' +and a factor 'Af' is generated from \code{Af <- At$factor()}, then +\code{Bt <- attr(Af, "CFTime")} describes the temporal dimension of the result +of, say, \code{B <- apply(A, 1:2, tapply, Af, FUN)}. The 'CFTime' attribute is +\code{NULL} for era factors. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$factor(period = "month", era = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{period}}{character. A character string with one of the values +"year", "season", "quarter", "month" (the default), "dekad" or "day".} + +\item{\code{era}}{numeric or list, optional. Vector of years for which to +construct the factor, or a list whose elements are each a vector of +years. If \code{era} is not specified, the factor will use the entire time +series for the factor.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +If \code{era} is a single vector or not specified, a factor with a +length equal to the number of offsets in this instance. If \code{era} is a +list, a list with the same number of elements and names as \code{era}, +each containing a factor. Elements in the factor will be set to \code{NA} +for time series values outside of the range of specified years. + +The factor, or factors in the list, have attributes 'period', 'era' +and 'CFTime'. Attribute 'period' holds the value of the \code{period} +argument. Attribute 'era' indicates the number of years that are +included in the era, or -1 if no \code{era} is provided. Attribute +'CFTime' holds an instance of \code{CFTime} that has the same definition as +this instance, but with offsets corresponding to the mid-point of +non-era factor levels; if the \code{era} argument is specified, +attribute 'CFTime' is \code{NULL}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-factor_units}{}}} +\subsection{Method \code{factor_units()}}{ +Given a factor as produced by \code{CFTime$factor()}, this method +will return a numeric vector with the number of time units in each +level of the factor. + +The result of this method is useful to convert between absolute and +relative values. Climate change anomalies, for instance, are usually +computed by differencing average values between a future period and a +baseline period. Going from average values back to absolute values for +an aggregate period (which is typical for temperature and +precipitation, among other variables) is easily done with the result of +this method, without having to consider the specifics of the calendar +of the data set. + +If the factor \code{f} is for an era (e.g. spanning multiple years and the +levels do not indicate the specific year), then the result will +indicate the number of time units of the period in a regular single +year. In other words, for an era of 2041-2060 and a monthly factor on a +standard calendar with a \code{days} unit, the result will be +\code{c(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)}. Leap days are thus +only considered for the \verb{366_day} and \code{all_leap} calendars. + +Note that this function gives the number of time units in each level of +the factor - the actual number of data points in the time series per +factor level may be different. Use \code{\link[=CFfactor_coverage]{CFfactor_coverage()}} to determine +the actual number of data points or the coverage of data points +relative to the factor level. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$factor_units(f)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{f}}{A factor or a list of factors derived from the method +\code{CFTime$factor()}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +If \code{f} is a factor, a numeric vector with a length equal to the +number of levels in the factor, indicating the number of time units in +each level of the factor. If \code{f} is a list of factors, a list with each +element a numeric vector as above. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-factor_coverage}{}}} +\subsection{Method \code{factor_coverage()}}{ +Calculate the number of time elements, or the relative +coverage, in each level of a factor generated by \code{CFTime$factor()}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$factor_coverage(f, coverage = "absolute")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{f}}{A factor or a list of factors derived from the method +\code{CFTime$factor()}.} + +\item{\code{coverage}}{"absolute" or "relative".} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +If \code{f} is a factor, a numeric vector with a length equal to the +number of levels in the factor, indicating the number of units from the +time series contained in each level of the factor when +\code{coverage = "absolute"} or the proportion of units present relative to the +maximum number when \code{coverage = "relative"}. If \code{f} is a list of factors, a +list with each element a numeric vector as above. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-CFTime-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{CFTime$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} } diff --git a/man/as.character-CFtime-method.Rd b/man/as.character-CFtime-method.Rd deleted file mode 100644 index 829e185..0000000 --- a/man/as.character-CFtime-method.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{as.character,CFtime-method} -\alias{as.character,CFtime-method} -\title{Return the timestamps contained in the CFtime instance.} -\usage{ -\S4method{as.character}{CFtime}(x) -} -\arguments{ -\item{x}{The CFtime instance whose timestamps will be returned} -} -\value{ -The timestamps in the specified CFtime instance. -} -\description{ -Return the timestamps contained in the CFtime instance. -} -\examples{ -cf <- CFtime("days since 1850-01-01", "julian", 0:364) -as.character(cf) -} diff --git a/man/as.character.CFTime.Rd b/man/as.character.CFTime.Rd new file mode 100644 index 0000000..2d2da2b --- /dev/null +++ b/man/as.character.CFTime.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/api.R +\name{as.character.CFTime} +\alias{as.character.CFTime} +\title{Return the timestamps contained in the \code{CFTime} instance.} +\usage{ +\method{as.character}{CFTime}(x, ...) +} +\arguments{ +\item{x}{The \code{CFTime} instance whose timestamps will be returned.} + +\item{...}{Ignored.} +} +\value{ +The timestamps in the specified \code{CFTime} instance. +} +\description{ +Return the timestamps contained in the \code{CFTime} instance. +} +\examples{ +t <- CFtime("days since 1850-01-01", "julian", 0:364) +as.character(t) +} diff --git a/man/as_timestamp.Rd b/man/as_timestamp.Rd index f0b4c84..0eb0b0c 100644 --- a/man/as_timestamp.Rd +++ b/man/as_timestamp.Rd @@ -1,21 +1,21 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFformat.R +% Please edit documentation in R/api.R \name{as_timestamp} \alias{as_timestamp} \title{Create a vector that represents CF timestamps} \usage{ -as_timestamp(cf, format = NULL, asPOSIX = FALSE) +as_timestamp(t, format = NULL, asPOSIX = FALSE) } \arguments{ -\item{cf}{CFtime. The \code{CFtime} instance that contains the offsets to use.} +\item{t}{The \code{CFTime} instance that contains the offsets to use.} -\item{format}{character. A character string with either of the values "date" or -"timestamp". If the argument is not specified, the format used is +\item{format}{character. A character string with either of the values "date" +or "timestamp". If the argument is not specified, the format used is "timestamp" if there is time information, "date" otherwise.} \item{asPOSIX}{logical. If \code{TRUE}, for "standard", "gregorian" and "proleptic_gregorian" calendars the output is a vector of \code{POSIXct} - for -other calendars the result is \code{NULL}. Default value is \code{FALSE}.} +other calendars an error will be thrown. Default value is \code{FALSE}.} } \value{ A character vector where each element represents a moment in time @@ -31,22 +31,23 @@ the \code{format} specifier. The date in the string is not necessarily compatibl with \code{POSIXt} - in the \verb{360_day} calendar \code{2017-02-30} is valid and \code{2017-03-31} is not. -For the "standard", "gregorian" and "proleptic_gregorian" calendars the -output can also be generated as a vector of \code{POSIXct} values by specifying -\code{asPOSIX = TRUE}. +For the "proleptic_gregorian" calendar the output can also be generated as a +vector of \code{POSIXct} values by specifying \code{asPOSIX = TRUE}. The same is +possible for the "standard" and "gregorian" calendars but only if all +timestamps fall on or after 1582-10-15. } \examples{ -cf <- CFtime("hours since 2020-01-01", "standard", seq(0, 24, by = 0.25)) -as_timestamp(cf, "timestamp") +t <- CFtime("hours since 2020-01-01", "standard", seq(0, 24, by = 0.25)) +as_timestamp(t, "timestamp") -cf2 <- CFtime("days since 2002-01-21", "standard", 0:20) -tail(as_timestamp(cf2, asPOSIX = TRUE)) +t2 <- CFtime("days since 2002-01-21", "standard", 0:20) +tail(as_timestamp(t2, asPOSIX = TRUE)) -tail(as_timestamp(cf2)) +tail(as_timestamp(t2)) -tail(as_timestamp(cf2 + 1.5)) +tail(as_timestamp(t2 + 1.5)) } \seealso{ -The \code{\link[=format]{format()}} function gives greater flexibility through -the use of strptime-like format specifiers. +The \link{CFTime} \code{format()} method gives greater flexibility through +the use of \code{strptime}-like format specifiers. } diff --git a/man/bounds.Rd b/man/bounds.Rd index 81e2852..58f2e43 100644 --- a/man/bounds.Rd +++ b/man/bounds.Rd @@ -1,22 +1,16 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R +% Please edit documentation in R/api.R \name{bounds} \alias{bounds} -\alias{bounds,CFtime-method} \alias{bounds<-} -\alias{bounds<-,CFtime-method} \title{Bounds of the time offsets} \usage{ bounds(x, format) -\S4method{bounds}{CFtime}(x, format) - bounds(x) <- value - -\S4method{bounds}{CFtime}(x) <- value } \arguments{ -\item{x}{A \code{CFtime} instance} +\item{x}{A \code{CFTime} instance.} \item{format}{Optional. A single string with format specifiers, see \code{\link[=format]{format()}} for details.} @@ -24,7 +18,7 @@ bounds(x) <- value \item{value}{A \code{matrix} (or \code{array}) with dimensions (2, length(offsets)) giving the lower (first row) and higher (second row) bounds of each offset (this is the format that the CF Metadata Conventions uses for storage in -NetCDF files). Use \code{FALSE} to unset any previously set bounds, \code{TRUE} to +netCDF files). Use \code{FALSE} to unset any previously set bounds, \code{TRUE} to set regular bounds at mid-points between the offsets (which must be regular as well).} } @@ -36,18 +30,18 @@ the upper bound, with each column representing an offset of \code{x}. If the according to the format. \code{NULL} when no bounds have been set. } \description{ -CF-compliant NetCDF files store time information as a single offset value for +CF-compliant netCDF files store time information as a single offset value for each step along the dimension, typically centered on the valid interval of the data (e.g. 12-noon for day data). Optionally, the lower and upper values of the valid interval are stored in a so-called "bounds" variable, as an array with two rows (lower and higher value) and a column for each offset. -With function \verb{bounds()<-} those bounds can be set for a CFtime instance. The -bounds can be retrieved with the \code{bounds()} function. +With function \verb{bounds()<-} those bounds can be set for a \code{CFTime} instance. +The bounds can be retrieved with the \code{bounds()} function. } \examples{ -cf <- CFtime("days since 2024-01-01", "standard", seq(0.5, by = 1, length.out = 366)) -as_timestamp(cf)[1:3] -bounds(cf) <- rbind(0:365, 1:366) -bounds(cf)[, 1:3] -bounds(cf, "\%d-\%b-\%Y")[, 1:3] +t <- CFtime("days since 2024-01-01", "standard", seq(0.5, by = 1, length.out = 366)) +as_timestamp(t)[1:3] +bounds(t) <- rbind(0:365, 1:366) +bounds(t)[, 1:3] +bounds(t, "\%d-\%b-\%Y")[, 1:3] } diff --git a/man/cut-CFtime-method.Rd b/man/cut.CFTime.Rd similarity index 66% rename from man/cut-CFtime-method.Rd rename to man/cut.CFTime.Rd index be651be..2752994 100644 --- a/man/cut-CFtime-method.Rd +++ b/man/cut.CFTime.Rd @@ -1,14 +1,14 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{cut,CFtime-method} -\alias{cut,CFtime-method} +% Please edit documentation in R/api.R +\name{cut.CFTime} +\alias{cut.CFTime} \alias{cut} -\title{Create a factor for a CFtime instance} +\title{Create a factor for a \code{CFTime} instance} \usage{ -\S4method{cut}{CFtime}(x, breaks, ...) +\method{cut}{CFTime}(x, breaks, ...) } \arguments{ -\item{x}{An instance of CFtime.} +\item{x}{An instance of \code{CFTime}.} \item{breaks}{A character string of a factor period (see \code{\link[=CFfactor]{CFfactor()}} for a description), or a character vector of timestamps that conform to the @@ -19,14 +19,14 @@ ISO8601 format, e.g. "2024-04-10 21:31:43".} } \value{ A factor with levels according to the \code{breaks} argument, with -attributes 'period', 'epoch' and 'CFtime'. When \code{breaks} is a factor +attributes 'period', 'era' and 'CFTime'. When \code{breaks} is a factor period, attribute 'period' has that value, otherwise it is '"day"'. When -\code{breaks} is a character vector of timestamps, attribute 'CFtime' holds an -instance of CFtime that has the same definition as \code{x}, but with (ordered) -offsets generated from the \code{breaks}. Attribute 'epoch' is always -1. +\code{breaks} is a character vector of timestamps, attribute 'CFTime' holds an +instance of \code{CFTime} that has the same definition as \code{x}, but with (ordered) +offsets generated from the \code{breaks}. Attribute 'era' is always -1. } \description{ -Method for \code{\link[base:cut]{base::cut()}} applied to CFtime objects. +Method for \code{\link[base:cut]{base::cut()}} applied to \link{CFTime} objects. } \details{ When \code{breaks} is one of \verb{"year", "season", "quarter", "month", "dekad", "day"} a factor is generated like by \code{\link[=CFfactor]{CFfactor()}}. @@ -35,11 +35,10 @@ When \code{breaks} is a vector of character timestamps a factor is produced with level for every interval between timestamps. The last timestamp, therefore, is only used to close the interval started by the pen-ultimate timestamp - use a distant timestamp (e.g. \code{range(x)[2]}) to ensure that all offsets to -the end of the CFtime time series are included, if so desired. The last -timestamp will become the upper bound in the CFtime instance that is returned -as an attribute to this function so a sensible value for the last timestamp -is advisable. The earliest timestamp cannot be earlier than the origin of the -datum of \code{x}. +the end of the CFTime time series are included, if so desired. The last +timestamp will become the upper bound in the \code{CFTime} instance that is +returned as an attribute to this function so a sensible value for the last +timestamp is advisable. This method works similar to \code{\link[base:cut.POSIXt]{base::cut.POSIXt()}} but there are some differences in the arguments: for \code{breaks} the set of options is different @@ -53,5 +52,5 @@ cut(x, breaks) } \seealso{ \code{\link[=CFfactor]{CFfactor()}} produces a factor for several fixed periods, including -for epochs. +for eras. } diff --git a/man/deprecated_functions.Rd b/man/deprecated_functions.Rd index a193f5b..b1e02ad 100644 --- a/man/deprecated_functions.Rd +++ b/man/deprecated_functions.Rd @@ -5,16 +5,22 @@ \alias{CFtimestamp} \alias{CFmonth_days} \alias{CFcomplete} +\alias{CFsubset} +\alias{CFparse} \title{Deprecated functions} \usage{ -CFtimestamp(cf, format = NULL, asPOSIX = FALSE) +CFtimestamp(t, format = NULL, asPOSIX = FALSE) -CFmonth_days(cf, x = NULL) +CFmonth_days(t, x = NULL) CFcomplete(x) + +CFsubset(x, extremes) + +CFparse(t, x) } \arguments{ -\item{cf, x, format, asPOSIX}{See replacement functions.} +\item{t, x, format, asPOSIX, extremes}{See replacement functions.} } \value{ See replacement functions. @@ -27,6 +33,7 @@ function if no arguments are given in the table.\tabular{ll}{ \strong{Deprecated function} \tab \strong{Replacement function} \cr CFcomplete() \tab \code{\link[=is_complete]{is_complete()}} \cr CFmonth_days() \tab \code{\link[=month_days]{month_days()}} \cr + CFparse() \tab \code{\link[=parse_timestamps]{parse_timestamps()}} \cr CFrange() \tab \code{\link[=range]{range()}} \cr CFsubset() \tab \code{\link[=slab]{slab()}} \cr CFtimestamp() \tab \code{\link[=as_timestamp]{as_timestamp()}} \cr diff --git a/man/equals-.CFTime.Rd b/man/equals-.CFTime.Rd new file mode 100644 index 0000000..fbee27a --- /dev/null +++ b/man/equals-.CFTime.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/api.R +\name{==.CFTime} +\alias{==.CFTime} +\alias{CFtime-equivalent} +\title{Equivalence of CFTime objects} +\usage{ +\method{==}{CFTime}(e1, e2) +} +\arguments{ +\item{e1, e2}{Instances of the \code{CFTime} class.} +} +\value{ +\code{TRUE} if the \code{CFTime} objects are equivalent, \code{FALSE} otherwise. +} +\description{ +This operator can be used to test if two \link{CFTime} objects represent the same +CF-convention time coordinates. Two \code{CFTime} objects are considered equivalent +if they have an equivalent calendar and the same offsets. +} +\examples{ +e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) +e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 0:364) +e1 == e2 +} diff --git a/man/equals-CFtime-CFtime-method.Rd b/man/equals-CFtime-CFtime-method.Rd deleted file mode 100644 index ddc60ca..0000000 --- a/man/equals-CFtime-CFtime-method.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{==,CFtime,CFtime-method} -\alias{==,CFtime,CFtime-method} -\alias{CFtime-equivalent} -\title{Equivalence of CFtime objects} -\usage{ -\S4method{==}{CFtime,CFtime}(e1, e2) -} -\arguments{ -\item{e1, e2}{CFtime. Instances of the \code{CFtime} class.} -} -\value{ -\code{TRUE} if the \code{CFtime} objects are equivalent, \code{FALSE} otherwise. -} -\description{ -This operator can be used to test if two \code{CFtime} objects represent the same -CF-convention time coordinates. Two \code{CFtime} objects are considered equivalent -if they have an equivalent datum and the same offsets. -} -\examples{ -e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) -e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 0:364) -e1 == e2 -} diff --git a/man/format-CFtime-method.Rd b/man/format-CFtime-method.Rd deleted file mode 100644 index cf2ee08..0000000 --- a/man/format-CFtime-method.Rd +++ /dev/null @@ -1,52 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{format,CFtime-method} -\alias{format,CFtime-method} -\title{Format time elements using format specifiers} -\usage{ -\S4method{format}{CFtime}(x, format) -} -\arguments{ -\item{x}{CFtime. A CFtime instance whose offsets will be returned as -timestamps.} - -\item{format}{character. A character string with strptime format -specifiers. If omitted, the most economical format will be used: a full -timestamp when time information is available, a date otherwise.} -} -\value{ -A vector of character strings with a properly formatted timestamp. -Any format specifiers not recognized or supported will be returned verbatim. -} -\description{ -Format timestamps using a specific format string, using the specifiers -defined for the \code{\link[base:strptime]{base::strptime()}} function, with limitations. The only -supported specifiers are \verb{bBdeFhHIjmMpRSTYz\%}. Modifiers \code{E} and \code{O} are -silently ignored. Other specifiers, including their percent sign, are copied -to the output as if they were adorning text. -} -\details{ -The formatting is largely oblivious to locale. The reason for this is that -certain dates in certain calendars are not POSIX-compliant and the system -functions necessary for locale information thus do not work consistently. The -main exception to this is the (abbreviated) names of months (\code{bB}), which -could be useful for pretty printing in the local language. For separators and -other locale-specific adornments, use local knowledge instead of depending on -system locale settings; e.g. specify \verb{\%m/\%d/\%Y} instead of \verb{\%D}. - -Week information, including weekday names, is not supported at all as a -"week" is not defined for non-standard CF calendars and not generally useful -for climate projection data. If you are working with observed data and want -to get pretty week formats, use the \code{\link[=as_timestamp]{as_timestamp()}} function to generate -\code{POSIXct} timestamps (observed data generally uses a standard calendar) and -then use the \code{\link[base:format]{base::format()}} function which supports the full set of -specifiers. -} -\examples{ -cf <- CFtime("days since 2020-01-01", "standard", 0:365) -format(cf, "\%Y-\%b") - -# Use system facilities on a standard calendar -format(as_timestamp(cf, asPOSIX = TRUE), "\%A, \%x") - -} diff --git a/man/indexOf-ANY-CFtime-method.Rd b/man/indexOf.Rd similarity index 60% rename from man/indexOf-ANY-CFtime-method.Rd rename to man/indexOf.Rd index db1e7a8..f7c43df 100644 --- a/man/indexOf-ANY-CFtime-method.Rd +++ b/man/indexOf.Rd @@ -1,17 +1,16 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{indexOf,ANY,CFtime-method} -\alias{indexOf,ANY,CFtime-method} +% Please edit documentation in R/api.R +\name{indexOf} \alias{indexOf} \title{Find the index of timestamps in the time series} \usage{ -\S4method{indexOf}{ANY,CFtime}(x, y, method = "constant") +indexOf(x, y, method = "constant") } \arguments{ -\item{x}{Vector of character, POSIXt or Date values to find indices for, or a -numeric vector.} +\item{x}{Vector of \code{character}, \code{POSIXt} or \code{Date} values to find indices +for, or a numeric vector.} -\item{y}{CFtime instance.} +\item{y}{\link{CFTime} instance.} \item{method}{Single value of "constant" or "linear". If \code{"constant"} or when bounds are set on argument \code{y}, return the index value for each match. If @@ -19,21 +18,19 @@ bounds are set on argument \code{y}, return the index value for each match. If } \value{ A numeric vector giving indices into the "time" dimension of the -dataset associated with \code{y} for the values of \code{x}. If there is at least 1 -valid index, then attribute "CFtime" -contains an instance of CFtime that describes the dimension of filtering -the dataset associated with \code{y} with the result of this function, excluding -any \code{NA}, \code{0} and \code{.Machine$integer.max} values. +data set associated with \code{y} for the values of \code{x}. If there is at least 1 +valid index, then attribute "CFTime" contains an instance of \code{CFTime} that +describes the dimension of filtering the data set associated with \code{y} with +the result of this function, excluding any \code{NA}, \code{0} and +\code{.Machine$integer.max} values. } \description{ -In the CFtime instance \code{y}, find the index in the time series for each -timestamp given in argument \code{x}. Values of \code{x} that are before the earliest -value in \code{y} will be returned as \code{0} (except when the value is before the -datum of \code{y}, in which case the value returned is \code{NA}); values of \code{x} that -are after the latest values in \code{y} will be returned as -\code{.Machine$integer.max}. Alternatively, when \code{x} is a numeric vector of index -values, return the valid indices of the same vector, with the side effect -being the attribute "CFtime" associated with the result. +Find the index in the time series for each timestamp given in argument \code{x}. +Values of \code{x} that are before the earliest value in \code{y} will be returned as +\code{0}; values of \code{x} that are after the latest values in \code{y} will be returned +as \code{.Machine$integer.max}. Alternatively, when \code{x} is a numeric vector of +index values, return the valid indices of the same vector, with the side +effect being the attribute "CFTime" associated with the result. } \details{ Timestamps can be provided as vectors of character strings, \code{POSIXct} or @@ -61,10 +58,10 @@ will be returned as \code{NA}. \code{x} can also be a numeric vector of index values, in which case the valid values in \code{x} are returned. If negative values are passed, the positive counterparts will be excluded and then the remainder returned. Positive and -negative values may not be mixed. Using a numeric vector has -the side effect that the result has the attribute "CFtime" describing the -temporal dimension of the slice. If index values outside of the range of \code{y} -(\code{1:length(y)}) are provided, an error will be thrown. +negative values may not be mixed. Using a numeric vector has the side effect +that the result has the attribute "CFTime" describing the temporal dimension +of the slice. If index values outside of the range of \code{y} (\code{1:length(y)}) are +provided, an error will be thrown. } \examples{ cf <- CFtime("days since 2020-01-01", "360_day", 1440:1799 + 0.5) diff --git a/man/is_complete.Rd b/man/is_complete.Rd index c6916f8..ec55659 100644 --- a/man/is_complete.Rd +++ b/man/is_complete.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R +% Please edit documentation in R/api.R \name{is_complete} \alias{is_complete} \title{Indicates if the time series is complete} @@ -7,11 +7,11 @@ is_complete(x) } \arguments{ -\item{x}{An instance of the \code{CFtime} class} +\item{x}{An instance of the \link{CFTime} class.} } \value{ logical. \code{TRUE} if the time series is complete, with no gaps; -\code{FALSE} otherwise. If no offsets have been added to the CFtime instance, +\code{FALSE} otherwise. If no offsets have been added to the \code{CFTime} instance, \code{NA} is returned. } \description{ @@ -20,18 +20,18 @@ steps are equally spaced and there are thus no gaps in the time series. } \details{ This function gives exact results for time series where the nominal -\emph{unit of separation} between observations in the time series is exact in terms of the -datum unit. As an example, for a datum unit of "days" where the observations -are spaced a fixed number of days apart the result is exact, but if the same -datum unit is used for data that is on a monthly basis, the \emph{assessment} is -approximate because the number of days per month is variable and dependent on -the calendar (the exception being the \verb{360_day} calendar, where the -assessment is exact). The \emph{result} is still correct in most cases (including -all CF-compliant data sets that the developers have seen) although there may -be esoteric constructions of CFtime and offsets that trip up this +\emph{unit of separation} between observations in the time series is exact in +terms of the calendar unit. As an example, for a calendar unit of "days" where the +observations are spaced a fixed number of days apart the result is exact, but +if the same calendar unit is used for data that is on a monthly basis, the +\emph{assessment} is approximate because the number of days per month is variable +and dependent on the calendar (the exception being the \verb{360_day} calendar, +where the assessment is exact). The \emph{result} is still correct in most cases +(including all CF-compliant data sets that the developers have seen) although +there may be esoteric constructions of CFTime and offsets that trip up this implementation. } \examples{ -cf <- CFtime("days since 1850-01-01", "julian", 0:364) -is_complete(cf) +t <- CFtime("days since 1850-01-01", "julian", 0:364) +is_complete(t) } diff --git a/man/length-CFtime-method.Rd b/man/length-CFtime-method.Rd deleted file mode 100644 index 8776c5c..0000000 --- a/man/length-CFtime-method.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{length,CFtime-method} -\alias{length,CFtime-method} -\title{The length of the offsets contained in the CFtime instance.} -\usage{ -\S4method{length}{CFtime}(x) -} -\arguments{ -\item{x}{The CFtime instance whose length will be returned} -} -\value{ -The number of offsets in the specified CFtime instance. -} -\description{ -The length of the offsets contained in the CFtime instance. -} -\examples{ -cf <- CFtime("days since 1850-01-01", "julian", 0:364) -length(cf) -} diff --git a/man/length.CFTime.Rd b/man/length.CFTime.Rd new file mode 100644 index 0000000..2e8e8e2 --- /dev/null +++ b/man/length.CFTime.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/api.R +\name{length.CFTime} +\alias{length.CFTime} +\title{The length of the offsets contained in the \code{CFTime} instance.} +\usage{ +\method{length}{CFTime}(x) +} +\arguments{ +\item{x}{The \code{CFTime} instance whose length will be returned} +} +\value{ +The number of offsets in the specified \code{CFTime} instance. +} +\description{ +The length of the offsets contained in the \code{CFTime} instance. +} +\examples{ +t <- CFtime("days since 1850-01-01", "julian", 0:364) +length(t) +} diff --git a/man/month_days.Rd b/man/month_days.Rd index dc6960d..b123769 100644 --- a/man/month_days.Rd +++ b/man/month_days.Rd @@ -1,47 +1,46 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFutils.R +% Please edit documentation in R/api.R \name{month_days} \alias{month_days} \title{Return the number of days in a month given a certain CF calendar} \usage{ -month_days(cf, x = NULL) +month_days(t, x = NULL) } \arguments{ -\item{cf}{CFtime. The CFtime definition to use.} +\item{t}{The \code{CFtime} instance to use.} \item{x}{character. An optional vector of dates as strings with format \code{YYYY-MM-DD}. Any time part will be silently ingested.} } \value{ A vector indicating the number of days in each month for the vector -of dates supplied as a parameter to the function. If no dates are supplied, -the number of days per month for the calendar as a vector of length 12. -Invalidly specified dates will result in an \code{NA} value. +of dates supplied as argument \verb{x. Invalidly specified dates will result in an }NA` value. If no dates are supplied, the number of days per month for +the calendar as a vector of length 12. } \description{ -Given a vector of dates as strings in ISO 8601 or UDUNITS format and a \code{CFtime} object, -this function will return a vector of the same length as the dates, -indicating the number of days in the month according to the calendar +Given a vector of dates as strings in ISO 8601 or UDUNITS format and a +\link{CFTime} object, this function will return a vector of the same length as the +dates, indicating the number of days in the month according to the calendar specification. If no vector of days is supplied, the function will return an integer vector of length 12 with the number of days for each month of the calendar (disregarding the leap day for \code{standard} and \code{julian} calendars). } \examples{ dates <- c("2021-11-27", "2021-12-10", "2022-01-14", "2022-02-18") -cf <- CFtime("days since 1850-01-01", "standard") -month_days(cf, dates) +t <- CFtime("days since 1850-01-01", "standard") +month_days(t, dates) -cf <- CFtime("days since 1850-01-01", "360_day") -month_days(cf, dates) +t <- CFtime("days since 1850-01-01", "360_day") +month_days(t, dates) -cf <- CFtime("days since 1850-01-01", "all_leap") -month_days(cf, dates) +t <- CFtime("days since 1850-01-01", "all_leap") +month_days(t, dates) -month_days(cf) +month_days(t) } \seealso{ When working with factors generated by \code{\link[=CFfactor]{CFfactor()}}, it is usually better to use \code{\link[=CFfactor_units]{CFfactor_units()}} as that will consider leap days for -non-epoch factors. \code{\link[=CFfactor_units]{CFfactor_units()}} can also work with other time periods -and datum units, such as "hours per month", or "days per season". +non-era factors. \code{\link[=CFfactor_units]{CFfactor_units()}} can also work with other time periods +and calendar units, such as "hours per month", or "days per season". } diff --git a/man/CFparse.Rd b/man/parse_timestamps.Rd similarity index 82% rename from man/CFparse.Rd rename to man/parse_timestamps.Rd index caf0ce6..c5b9a5c 100644 --- a/man/CFparse.Rd +++ b/man/parse_timestamps.Rd @@ -1,23 +1,22 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFparse.R -\name{CFparse} -\alias{CFparse} +% Please edit documentation in R/api.R +\name{parse_timestamps} +\alias{parse_timestamps} \title{Parse series of timestamps in CF format to date-time elements} \usage{ -CFparse(cf, x) +parse_timestamps(t, x) } \arguments{ -\item{cf}{CFtime. An instance of \code{CFtime} indicating the CF calendar and -datum to use when parsing the date.} +\item{t}{An instance of \code{CFTime} to use when parsing the date.} -\item{x}{character. Vector of character strings representing timestamps in +\item{x}{Vector of character strings representing timestamps in ISO8601 extended or UDUNITS broken format.} } \value{ -A data frame with constituent elements of the parsed timestamps in +A \code{data.frame} with constituent elements of the parsed timestamps in numeric format. The columns are year, month, day, hour, minute, second (with an optional fraction), time zone (character string), and the -corresponding offset value from the datum. Invalid input data will appear +corresponding offset value from the origin. Invalid input data will appear as \code{NA} - if this is the case, a warning message will be displayed - other missing information on input will use default values. } @@ -57,15 +56,12 @@ interpreted as "00:00"). Currently only the extended formats (with separators between the elements) are supported. The vector of timestamps may have any combination of ISO8601 and UDUNITS formats. - -Timestamps that are prior to the datum are not allowed. The corresponding row -in the result will have \code{NA} values. } \examples{ -cf <- CFtime("days since 0001-01-01", "proleptic_gregorian") +t <- CFtime("days since 0001-01-01", "proleptic_gregorian") # This will have `NA`s on output and generate a warning timestamps <- c("2012-01-01T12:21:34Z", "12-1-23", "today", "2022-08-16T11:07:34.45-10", "2022-08-16 10.5+04") -CFparse(cf, timestamps) +parse_timestamps(t, timestamps) } diff --git a/man/plus-.CFTime.Rd b/man/plus-.CFTime.Rd new file mode 100644 index 0000000..05c4cf4 --- /dev/null +++ b/man/plus-.CFTime.Rd @@ -0,0 +1,62 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/api.R +\name{+.CFTime} +\alias{+.CFTime} +\alias{CFtime-merge} +\title{Extend a CFTime object} +\usage{ +\method{+}{CFTime}(e1, e2) +} +\arguments{ +\item{e1}{Instance of the \code{CFTime} class.} + +\item{e2}{Instance of the \code{CFTime} class with a calendar compatible with that +of argument \code{e1}, or a numeric vector with offsets from the origin of +argument \code{e1}, or a vector of \code{character} timestamps in ISO8601 or UDUNITS +format.} +} +\value{ +A \code{CFTime} object with the offsets of argument \code{e1} extended by the +values from argument \code{e2}. +} +\description{ +A \link{CFTime} instance can be extended with this operator, using values from +another \code{CFTime} instance, or a vector of numeric offsets or character +timestamps. If the values come from another \code{CFTime} instance, the calendars +of the two instances must be compatible If the calendars of the \code{CFTime} +instances are not compatible, an error is thrown. +} +\details{ +The resulting \code{CFTime} instance will have the offsets of the original +\code{CFTime} instance, appended with offsets from argument \code{e2} in the order that +they are specified. If the new sequence of offsets is not monotonically +increasing a warning is generated (the COARDS metadata convention requires +offsets to be monotonically increasing). + +There is no reordering or removal of duplicates. This is because the time +series are usually associated with a data set and the correspondence between +the data in the files and the \code{CFTime} instance is thus preserved. When +merging the data sets described by this time series, the order must be +identical to the merging here. + +Note that when adding multiple vectors of offsets to a \code{CFTime} instance, it +is more efficient to first concatenate the vectors and then do a final +addition to the \code{CFTime} instance. So avoid +\code{CFtime(definition, calendar, e1) + CFtime(definition, calendar, e2) + CFtime(definition, calendar, e3) + ...} +but rather do \code{CFtime(definition, calendar) + c(e1, e2, e3, ...)}. It is the +responsibility of the operator to ensure that the offsets of the different +data sets are in reference to the same calendar. + +Note also that \code{RNetCDF} and \code{ncdf4} packages both return the values of the +"time" dimension as a 1-dimensional array. You have to \code{dim(time_values) <- NULL} to de-class the array to a vector before adding offsets to an existing +\code{CFtime} instance. + +Any bounds that were set will be removed. Use \code{\link[=bounds]{bounds()}} to retrieve the +bounds of the individual \code{CFTime} instances and then set them again after +merging the two instances. +} +\examples{ +e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) +e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 365:729) +e1 + e2 +} diff --git a/man/plus-CFtime-CFtime-method.Rd b/man/plus-CFtime-CFtime-method.Rd deleted file mode 100644 index 3591eeb..0000000 --- a/man/plus-CFtime-CFtime-method.Rd +++ /dev/null @@ -1,45 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{+,CFtime,CFtime-method} -\alias{+,CFtime,CFtime-method} -\alias{CFtime-merge} -\title{Merge two CFtime objects} -\usage{ -\S4method{+}{CFtime,CFtime}(e1, e2) -} -\arguments{ -\item{e1, e2}{CFtime. Instances of the \code{CFtime} class.} -} -\value{ -A \code{CFtime} object with a set of offsets composed of the offsets of -the instances of \code{CFtime} that the operator operates on. If the datum units -or calendars of the \code{CFtime} instances are not equivalent, an error is -thrown. -} -\description{ -Two \code{CFtime} instances can be merged into one with this operator, provided -that the units and calendars of the datums of the two instances are -equivalent. -} -\details{ -If the origins of the two datums are not identical, the earlier origin is -preserved and the offsets of the later origin are updated in the resulting -CFtime instance. - -The order of the two parameters is indirectly significant. The resulting -\code{CFtime} instance will have the offsets of both instances in the order that -they are specified. There is no reordering or removal of duplicates. This is -because the time series are usually associated with a data set and the -correspondence between the data in the files and the CFtime instance is thus -preserved. When merging the data sets described by this time series, the -order must be identical to the merging here. - -Any bounds that were set will be removed. Use \code{\link[=bounds]{bounds()}} to retrieve -the bounds of the individual \code{CFtime} instances and then set them again after -merging the two instances. -} -\examples{ -e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) -e2 <- CFtime("days since 1850-01-01 00:00:00", "standard", 365:729) -e1 + e2 -} diff --git a/man/plus-CFtime-numeric-method.Rd b/man/plus-CFtime-numeric-method.Rd deleted file mode 100644 index 0e1458d..0000000 --- a/man/plus-CFtime-numeric-method.Rd +++ /dev/null @@ -1,53 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{+,CFtime,numeric-method} -\alias{+,CFtime,numeric-method} -\alias{CFtime-append} -\title{Extend a CFtime object with additional offsets} -\usage{ -\S4method{+}{CFtime,numeric}(e1, e2) -} -\arguments{ -\item{e1}{CFtime. Instance of the \code{CFtime} class.} - -\item{e2}{numeric. Vector of offsets to be added to the \code{CFtime} instance.} -} -\value{ -A \code{CFtime} object with offsets composed of the \code{CFtime} instance and -the numeric vector. -} -\description{ -A \code{CFtime} instance can be extended by adding additional offsets using this -operator. -} -\details{ -The resulting \code{CFtime} instance will have its offsets in the order that they -are added, meaning that the offsets from the \code{CFtime} instance come first and -those from the numeric vector follow. There is no reordering or removal of -duplicates. This is because the time series are usually associated with a -data set and the correspondence between the two is thus preserved, if and -only if the data sets are merged in the same order. - -Note that when adding multiple vectors of offsets to a \code{CFtime} instance, it -is more efficient to first concatenate the vectors and then do a final -addition to the \code{CFtime} instance. So avoid \code{CFtime(definition, calendar, e1) + CFtime(definition, calendar, e2) + CFtime(definition, calendar, e3) + ...} -but rather do \code{CFtime(definition, calendar) + c(e1, e2, e3, ...)}. It is the -responsibility of the operator to ensure that the offsets of the different -data sets are in reference to the same datum. - -Note also that \code{RNetCDF} and \code{ncdf4} packages both return the values of the -"time" dimension as a 1-dimensional array. You have to \code{dim(time_values) <- NULL} -to de-class the array to a vector before adding offsets to an existing CFtime -instance. - -Negative offsets will generate an error. - -Any bounds that were set will be removed. Use \code{\link[=bounds]{bounds()}} to retrieve -the bounds of the individual \code{CFtime} instances and then set them again after -merging the two instances. -} -\examples{ -e1 <- CFtime("days since 1850-01-01", "gregorian", 0:364) -e2 <- 365:729 -e1 + e2 -} diff --git a/man/properties.Rd b/man/properties.Rd index 2183aad..a886d3d 100644 --- a/man/properties.Rd +++ b/man/properties.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R +% Please edit documentation in R/api.R \name{definition} \alias{definition} \alias{properties} @@ -9,61 +9,61 @@ \alias{timezone} \alias{offsets} \alias{resolution} -\title{Properties of a CFtime object} +\title{Properties of a CFTime object} \usage{ -definition(cf) +definition(t) -calendar(cf) +calendar(t) -unit(cf) +unit(t) -origin(cf) +origin(t) -timezone(cf) +timezone(t) -offsets(cf) +offsets(t) -resolution(cf) +resolution(t) } \arguments{ -\item{cf}{CFtime. An instance of \code{CFtime}.} +\item{t}{An instance of \code{CFTime}.} } \value{ \code{calendar()} and \code{unit()} return a character string. \code{origin()} returns a data frame of timestamp elements with a single row -of data. \code{timezone()} returns the datum time zone as a character +of data. \code{timezone()} returns the calendar time zone as a character string. \code{offsets()} returns a vector of offsets or \code{NULL} if no offsets have been set. } \description{ These functions return the properties of an instance of the -\code{CFtime} class. The properties are all read-only, but offsets can be added +\link{CFTime} class. The properties are all read-only, but offsets can be added using the \code{+} operator. } \section{Functions}{ \itemize{ -\item \code{definition()}: The definition string of the CFtime instance +\item \code{definition()}: The definition string of the \code{CFTime} instance. -\item \code{calendar()}: The calendar of the CFtime instance +\item \code{calendar()}: The calendar of the \code{CFTime} instance. -\item \code{unit()}: The unit of the CFtime instance +\item \code{unit()}: The unit of the \code{CFTime} instance. -\item \code{origin()}: The origin of the CFtime instance in timestamp elements +\item \code{origin()}: The origin of the \code{CFTime} instance in timestamp elements. -\item \code{timezone()}: The time zone of the datum of the CFtime instance as a character string +\item \code{timezone()}: The time zone of the calendar of the \code{CFTime} instance as a character string. -\item \code{offsets()}: The offsets of the CFtime instance as a vector +\item \code{offsets()}: The offsets of the \code{CFTime} instance as a numeric vector. -\item \code{resolution()}: The average separation between the offsets in the CFtime instance +\item \code{resolution()}: The average separation between the offsets in the \code{CFTime} instance. }} \examples{ -cf <- CFtime("days since 1850-01-01", "julian", 0:364) -definition(cf) -calendar(cf) -unit(cf) -timezone(cf) -origin(cf) -offsets(cf) -resolution(cf) +t <- CFtime("days since 1850-01-01", "julian", 0:364) +definition(t) +calendar(t) +unit(t) +timezone(t) +origin(t) +offsets(t) +resolution(t) } diff --git a/man/range-CFtime-method.Rd b/man/range.CFTime.Rd similarity index 77% rename from man/range-CFtime-method.Rd rename to man/range.CFTime.Rd index f6c5809..01ef2ac 100644 --- a/man/range-CFtime-method.Rd +++ b/man/range.CFTime.Rd @@ -1,13 +1,13 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R -\name{range,CFtime-method} -\alias{range,CFtime-method} +% Please edit documentation in R/api.R +\name{range.CFTime} +\alias{range.CFTime} \title{Extreme time series values} \usage{ -\S4method{range}{CFtime}(x, format = "", bounds = FALSE, ..., na.rm = FALSE) +\method{range}{CFTime}(x, format = "", bounds = FALSE, ..., na.rm = FALSE) } \arguments{ -\item{x}{An instance of the \code{CFtime} class.} +\item{x}{An instance of the \link{CFTime} class.} \item{format}{A character string with format specifiers, optional. If it is missing or an empty string, the most economical ISO8601 format is chosen: @@ -22,10 +22,11 @@ used, if set. Defaults to \code{FALSE}.} \item{na.rm}{Ignored.} } \value{ -Vector of two character representations of the extremes of the time series. +Vector of two character representations of the extremes of the time +series. } \description{ -Character representation of the extreme values in the time series +Character representation of the extreme values in the time series. } \examples{ cf <- CFtime("days since 1850-01-01", "julian", 0:364) diff --git a/man/slab.Rd b/man/slab.Rd index eb3be30..a78da23 100644 --- a/man/slab.Rd +++ b/man/slab.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFtime.R +% Please edit documentation in R/api.R \name{slab} \alias{slab} \title{Which time steps fall within two extreme values} @@ -7,12 +7,12 @@ slab(x, extremes, rightmost.closed = FALSE) } \arguments{ -\item{x}{CFtime. The time series to operate on.} +\item{x}{The \code{CFTime} instance to operate on.} -\item{extremes}{character. Vector of two timestamps that represent the +\item{extremes}{Character vector of two timestamps that represent the extremes of the time period of interest. The timestamps must be in increasing order. The timestamps need not fall in the range of the time -steps in the CFtime stance.} +steps in argument `x.} \item{rightmost.closed}{Is the larger extreme value included in the result? Default is \code{FALSE}.} @@ -26,7 +26,7 @@ will thus include all time steps that fall in the year 2022. } \description{ Given two extreme character timestamps, return a logical vector of a length -equal to the number of time steps in the CFtime instance with values \code{TRUE} +equal to the number of time steps in the \link{CFTime} instance with values \code{TRUE} for those time steps that fall between the two extreme values, \code{FALSE} otherwise. This can be used to select slices from the time series in reading or analysing data. @@ -35,6 +35,6 @@ or analysing data. If bounds were set these will be preserved. } \examples{ -cf <- CFtime("hours since 2023-01-01 00:00:00", "standard", 0:23) -slab(cf, c("2022-12-01", "2023-01-01 03:00")) +t <- CFtime("hours since 2023-01-01 00:00:00", "standard", 0:23) +slab(t, c("2022-12-01", "2023-01-01 03:00")) } diff --git a/man/str.CFdatum.Rd b/man/str.CFdatum.Rd deleted file mode 100644 index 5071169..0000000 --- a/man/str.CFdatum.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/CFutils.R -\name{str.CFdatum} -\alias{str.CFdatum} -\title{Display the structure of a CFdatum instance} -\usage{ -\method{str}{CFdatum}(object, ...) -} -\arguments{ -\item{object}{\code{CFdatum} instance to print structure of.} - -\item{...}{Ignored.} -} -\value{ -Nothing. Prints information to the console. -} -\description{ -Display the structure of a CFdatum instance -} diff --git a/tests/testthat/test-CFbounds.R b/tests/testthat/test-CFbounds.R index 87eae99..9a1099d 100644 --- a/tests/testthat/test-CFbounds.R +++ b/tests/testthat/test-CFbounds.R @@ -1,37 +1,37 @@ test_that("bounds works", { - cf <- CFtime("days since 2024-01-01", "standard") + t <- CFtime("days since 2024-01-01", "standard") off <- seq(from = 0.5, by = 1, length.out = 10) bnds <- rbind(0:9, 1:10) - expect_null(bounds(cf)) # no offsets - expect_error(bounds(cf) <- bnds) # + expect_null(bounds(t)) # no offsets + expect_error(bounds(t) <- bnds) # - cf <- cf + off - expect_null(bounds(cf)) # bounds not set - bounds(cf) <- TRUE - expect_equal(bounds(cf), bnds) - expect_equal(bounds(cf, "%Y-%m-%d")[2,1:3], c("2024-01-02", "2024-01-03", "2024-01-04")) - bounds(cf) <- FALSE - expect_null(bounds(cf)) + t <- t + off + expect_null(bounds(t)) # bounds not set + bounds(t) <- TRUE + expect_equal(bounds(t), bnds) + expect_equal(bounds(t, "%Y-%m-%d")[2,1:3], c("2024-01-02", "2024-01-03", "2024-01-04")) + bounds(t) <- FALSE + expect_null(bounds(t)) - expect_error(bounds(cf) <- matrix(1:12, nrow = 4)) - expect_error(bounds(cf) <- "plain wrong") - expect_error(bounds(cf) <- bnds * 10) + expect_error(bounds(t) <- matrix(1:12, nrow = 4)) + expect_error(bounds(t) <- "plain wrong") + expect_error(bounds(t) <- bnds * 10) - bounds(cf) <- bnds - expect_match(capture_output(methods::show(cf)), "Bounds : regular and consecutive$") + bounds(t) <- bnds + expect_match(capture_output(t$print()), "Bounds : regular and consecutive$") hr6 <- rbind(off - 0.25, off + 0.25) - bounds(cf) <- hr6 - expect_match(capture_output(methods::show(cf)), "Bounds : irregular$") - expect_equal(bounds(cf), hr6) - expect_equal(bounds(cf, "%H")[,1], c("06", "18")) + bounds(t) <- hr6 + expect_match(capture_output(t$print()), "Bounds : irregular$") + expect_equal(bounds(t), hr6) + expect_equal(bounds(t, "%H")[,1], c("06", "18")) }) test_that("indexOf() works", { off <- (23*360):(24*360-1) + 0.5 # year 2024 days - cf <- CFtime("days since 2001-01-01", "360_day", off) - x <- c("1999-02-12", # pre-datum + t <- CFtime("days since 2001-01-01", "360_day", off) + x <- c("1999-02-12", # pre-origin "2023-06-23", # pre-time series "2024-01-30", "2024-01-31", # non-existent date @@ -40,31 +40,31 @@ test_that("indexOf() works", { "2024-03-01", "2025-01-01") # post-time series - expect_error(indexOf(x, cf, method = 4)) - expect_error(indexOf(TRUE, cf)) + expect_error(indexOf(x, t, method = 4)) + expect_error(indexOf(TRUE, t)) expect_error(indexOf(x, CFtime("months since 2001-01-01", "standard", 0:23))) - expect_error(indexOf(x, cf, nomatch = "July")) # must be able to coerce to numeric + expect_error(indexOf(x, t, nomatch = "July")) # must be able to coerce to numeric - expect_equal(indexOf(x, cf)[1:8], c(NA, 0, 29, NA, 30, 59, 60, .Machine$integer.max)) - expect_equal(indexOf(x, cf, method = "linear")[1:8], c(NA, 0, 29.5, NA, 30.5, 59.5, 60.5, .Machine$integer.max)) + expect_equal(indexOf(x, t)[1:8], c(0, 0, 29, NA, 30, 59, 60, .Machine$integer.max)) + expect_equal(indexOf(x, t, method = "linear")[1:8], c(0, 0, 29.5, NA, 30.5, 59.5, 60.5, .Machine$integer.max)) n <- 1:3 - out <- indexOf(n, cf) - outcf <- attr(out, "CFtime") - expect_equal(as_timestamp(cf)[1:3], as_timestamp(outcf)) + out <- indexOf(n, t) + outt <- attr(out, "CFTime") + expect_equal(as_timestamp(t)[1:3], as_timestamp(outt)) n <- c(-1, -2, -3) - out <- indexOf(n, cf) - outcf <- attr(out, "CFtime") - expect_equal(length(outcf), 360 - 3) - expect_equal(as_timestamp(cf)[4:6], as_timestamp(outcf)[1:3]) - expect_error(indexOf(c(-1, 1), cf)) + out <- indexOf(n, t) + outt <- attr(out, "CFTime") + expect_equal(length(outt), 360 - 3) + expect_equal(as_timestamp(t)[4:6], as_timestamp(outt)[1:3]) + expect_error(indexOf(c(-1, 1), t)) # Attached CFtime must have valid timestamps in `x` - out <- indexOf(x, cf) - outcf <- attr(out, "CFtime") - expect_equal(as_timestamp(outcf), x[!is.na(out) & out > 0 & out < .Machine$integer.max]) + out <- indexOf(x, t) + outt <- attr(out, "CFTime") + expect_equal(as_timestamp(outt), x[!is.na(out) & out > 0 & out < .Machine$integer.max]) - bounds(cf) <- TRUE - expect_equal(indexOf(x, cf)[1:8], c(NA, 0, 30, NA, 31, 60, 61, .Machine$integer.max)) - expect_equal(indexOf(x, cf, method = "linear")[1:8], c(NA, 0, 30, NA, 31, 60, 61, .Machine$integer.max)) + bounds(t) <- TRUE + expect_equal(indexOf(x, t)[1:8], c(0, 0, 30, NA, 31, 60, 61, .Machine$integer.max)) + expect_equal(indexOf(x, t, method = "linear")[1:8], c(0, 0, 30, NA, 31, 60, 61, .Machine$integer.max)) }) diff --git a/tests/testthat/test-CFformat.R b/tests/testthat/test-CFformat.R index 67171a2..436d1af 100644 --- a/tests/testthat/test-CFformat.R +++ b/tests/testthat/test-CFformat.R @@ -59,7 +59,7 @@ test_that("CFfactor testing", { f <- CFfactor(cf, CFt$factor_periods[p]) expect_equal(as.character(f)[1L], first[p]) expect_equal(as.character(f)[7300L], last[p]) - newcf <- attr(f, "CFtime") + newcf <- attr(f, "CFTime") bnds <- bounds(newcf) expect_equal(definition(cf), definition(newcf)) expect_true(is.matrix(bnds)) @@ -67,16 +67,16 @@ test_that("CFfactor testing", { expect_equal(dim(bnds), c(2, np[p])) } - # Epoch factors for all available periods - epochs <- list(first = 2001L, double = 2002L:2003L, final3 = 2018L:2020L, outside = 2022L) + # Era factors for all available periods + eras <- list(first = 2001L, double = 2002L:2003L, final3 = 2018L:2020L, outside = 2022L) lvls <- c(1L, 4L, 4L, 12L, 36L, 365L) for (p in 1:6) { # year, season, quarter, month, dekad, day - f <- CFfactor(cf, CFt$factor_periods[p], epochs) + f <- CFfactor(cf, CFt$factor_periods[p], eras) expect_type(f, "list") expect_equal(length(f), 4L) expect_equal(length(f$first), 7300L) expect_equal(attr(f$first, "period"), CFt$factor_periods[p]) - expect_equal(attr(f$first, "epoch"), 1L) + expect_equal(attr(f$first, "era"), 1L) expect_null(attr(f$first, "zxcv")) if (p == 1L) { expect_equal(length(levels(f$first)), 1L) @@ -96,14 +96,14 @@ test_that("CFfactor testing", { } } - # Single epoch value for all available periods + # Single era value for all available periods for (p in 1:6) { # year, season, quarter, month, dekad, day f <- CFfactor(cf, CFt$factor_periods[p], 2002L) expect_s3_class(f, "factor") expect_equal(length(f), 7300L) expect_equal(length(levels(f)), lvls[p]) expect_equal(attr(f, "period"), CFt$factor_periods[p]) - expect_equal(attr(f, "epoch"), 1L) + expect_equal(attr(f, "era"), 1L) expect_null(attr(f, "zxcv")) } @@ -163,13 +163,13 @@ test_that("CFfactor testing", { expect_true(all(CFfactor_coverage(cf360, f, "absolute") == 30L)) expect_true(all(CFfactor_coverage(cf360, f, "relative") == 1L)) - # Units and coverage in factor levels with epochs - f <- CFfactor(cf, "year", epochs) + # Units and coverage in factor levels with eras + f <- CFfactor(cf, "year", eras) expect_true(all(unlist(CFfactor_units(cf, f)) == rep(365L, 6L))) expect_true(all(unlist(CFfactor_coverage(cf, f, "absolute")) == c(rep(365L, 6L), 0L))) expect_equal(sum(sapply(CFfactor_coverage(cf, f, "relative"), sum)), 3L) - f <- CFfactor(cf, "season", epochs) + f <- CFfactor(cf, "season", eras) expect_true(all(sapply(CFfactor_units(cf, f), function(x) {all(x == c(90L, 92L, 92L, 91L))}))) x <- CFfactor_coverage(cf, f, "absolute") expect_equal(x$first[1L], 59L) # Jan + Feb of first year at beginning of time series @@ -179,7 +179,7 @@ test_that("CFfactor testing", { #expect_equal(x[1], 59 / 90). # works in the console but not here expect_true(all(x[2L:12L] == 1L)) - f <- CFfactor(cf, "month", epochs) + f <- CFfactor(cf, "month", eras) expect_true(all(sapply(CFfactor_units(cf, f), function(x) {all(x == month_days)}))) x <- CFfactor_coverage(cf, f, "absolute") expect_true(all(x$first == month_days)) @@ -187,7 +187,7 @@ test_that("CFfactor testing", { expect_true(all(x$final3 == month_days * 3L)) expect_true(all(unlist(CFfactor_coverage(cf, f, "relative")) == 1L)) - f <- CFfactor(cf, "dekad", epochs) + f <- CFfactor(cf, "dekad", eras) expect_true(all(sapply(CFfactor_units(cf, f), function(x) {all(x == dekad_days)}))) x <- CFfactor_coverage(cf, f, "absolute") expect_true(all(x$first == dekad_days)) @@ -195,7 +195,7 @@ test_that("CFfactor testing", { expect_true(all(x$final3 == dekad_days * 3L)) expect_true(all(unlist(CFfactor_coverage(cf, f, "relative")) == 1L)) - f <- CFfactor(cf, "day", epochs) + f <- CFfactor(cf, "day", eras) expect_true(all(unlist(CFfactor_units(cf, f)) == 1L)) x <- CFfactor_coverage(cf, f, "absolute") expect_true(all(x$first == 1L)) @@ -205,12 +205,12 @@ test_that("CFfactor testing", { # all_leap calendar cf366 <- CFtime("days since 2001-01-01", "all_leap", 0:7319) - f <- CFfactor(cf366, "year", epochs) + f <- CFfactor(cf366, "year", eras) expect_true(all(unlist(CFfactor_units(cf366, f)) == rep(366L, 6L))) expect_true(all(unlist(CFfactor_coverage(cf366, f, "absolute")) == c(rep(366L, 6L), 0L))) expect_equal(sum(sapply(CFfactor_coverage(cf366, f, "relative"), sum)), 3L) - f <- CFfactor(cf366, "season", epochs) + f <- CFfactor(cf366, "season", eras) expect_true(all(sapply(CFfactor_units(cf366, f), function(x) {all(x == c(91L, 92L, 92L, 91L))}))) x <- CFfactor_coverage(cf366, f, "absolute") expect_equal(x$first[1L], 60L) # Jan + Feb of first year at beginning of time series @@ -220,7 +220,7 @@ test_that("CFfactor testing", { #expect_equal(x[1], 60 / 90). # works in the console but not here expect_true(all(x[2L:12L] == 1L)) - f <- CFfactor(cf366, "month", epochs) + f <- CFfactor(cf366, "month", eras) expect_true(all(sapply(CFfactor_units(cf366, f), function(x) {all(x == leap_month_days)}))) x <- CFfactor_coverage(cf366, f, "absolute") expect_true(all(x$first == leap_month_days)) @@ -228,7 +228,7 @@ test_that("CFfactor testing", { expect_true(all(x$final3 == leap_month_days * 3L)) expect_true(all(unlist(CFfactor_coverage(cf366, f, "relative")) == 1L)) - f <- CFfactor(cf366, "dekad", epochs) + f <- CFfactor(cf366, "dekad", eras) expect_true(all(sapply(CFfactor_units(cf366, f), function(x) {all(x == leap_dekad_days)}))) x <- CFfactor_coverage(cf366, f, "absolute") expect_true(all(x$first == leap_dekad_days)) @@ -236,7 +236,7 @@ test_that("CFfactor testing", { expect_true(all(x$final3 == leap_dekad_days * 3L)) expect_true(all(unlist(CFfactor_coverage(cf366, f, "relative")) == 1L)) - f <- CFfactor(cf366, "day", epochs) + f <- CFfactor(cf366, "day", eras) expect_true(all(unlist(CFfactor_units(cf366, f)) == 1L)) x <- CFfactor_coverage(cf366, f, "absolute") expect_true(all(x$first == 1L)) @@ -248,7 +248,7 @@ test_that("CFfactor testing", { n <- 365L * 20L cov <- 0.8 offsets <- sample(0L:(n-1L), n * cov) - cf <- CFtime("days since 2020-01-01", "365_day", offsets) + expect_warning(cf <- CFtime("days since 2020-01-01", "365_day", offsets)) f <- CFfactor(cf, "month") x <- CFfactor_coverage(cf, f, "absolute") expect_equal(sum(x), n * cov) @@ -263,7 +263,6 @@ test_that("cut() works", { expect_error(cut(cf, breaks = 5)) expect_error(cut(cf, "")) expect_error(cut(cf, "blah")) - expect_error(suppressWarnings(cut(cf, c("1900-01-01", "2020-04-03")))) # pre-datum break f <- cut(cf, "quarter") expect_equal(nlevels(f), 8) diff --git a/tests/testthat/test-CFtime.R b/tests/testthat/test-CFtime.R index 04d2463..0711fe3 100644 --- a/tests/testthat/test-CFtime.R +++ b/tests/testthat/test-CFtime.R @@ -14,117 +14,120 @@ test_that("test all variants of creating a CFtime object and useful functions", expect_equal(resolution(CFtime("days since 1991-01-01", NULL, NULL)), NA_real_) # Call CFtime() with a valid definition and calendar but faulty offsets - expect_error(CFtime("days since 1991-01-01", "standard", -9:10)) expect_error(CFtime("days since 1991-01-01", "standard", c(0:10, NA))) - expect_error(CFtime("days since 1991-01-01", offsets = -9:10)) - expect_error(CFtime("days since 1991-01-01", offsets = 3.4e24)) # CFtime() with only a definition - cf <- CFtime("d per 1991-01-01") + t <- CFtime("d per 1991-01-01") - expect_equal(origin(cf)[1:3], data.frame(year = 1991, month = 1, day = 1)) - expect_equal(unit(cf), "days") - expect_equal(calendar(cf), "standard") - expect_equal(length(offsets(cf)), 0L) + expect_equal(origin(t)[1:3], data.frame(year = 1991, month = 1, day = 1)) + expect_equal(unit(t), "days") + expect_equal(calendar(t), "standard") + expect_equal(length(offsets(t)), 0L) # CFtime with only a definition and a calendar - cf <- CFtime("d per 1991-01-01", "julian") - expect_match(capture_output(methods::show(cf)), "^CF datum of origin:") - expect_match(capture_output(methods::show(cf)), "Elements: \\(no elements\\)\\n Bounds : \\(not set\\)$") + t <- CFtime("d per 1991-01-01", "julian") + expect_match(capture_output(t$print()), "^CF calendar:") + expect_match(capture_output(t$print()), "Elements: \\(no elements\\)\\n Bounds : \\(not set\\)$") - expect_equal(origin(cf)[1:3], data.frame(year = 1991, month = 1, day = 1)) - expect_equal(unit(cf), "days") - expect_equal(calendar(cf), "julian") - expect_equal(length(offsets(cf)), 0L) + expect_equal(origin(t)[1:3], data.frame(year = 1991, month = 1, day = 1)) + expect_equal(unit(t), "days") + expect_equal(calendar(t), "julian") + expect_equal(length(offsets(t)), 0L) # CFtime with a single offset - cf <- cf + 15 - expect_equal(as_timestamp(cf, "date"), "1991-01-16") - expect_match(capture_output(methods::show(cf)), "Elements: 1991-01-16 \\n Bounds : not set$") + t <- t + 15 + expect_equal(as_timestamp(t, "date"), "1991-01-16") + expect_match(capture_output(t$print()), "Elements: 1991-01-16 \\n Bounds : not set$") # Invalid offsets expect_error(CFtime("d per 1991-01-01", "julian", c(TRUE, FALSE, FALSE))) # Character offsets - cf <- CFtime("hours since 2023-01-01", "360_day", "2023-04-30T23:00") - expect_equal(range(cf), c("2023-01-01 00:00:00", "2023-04-30 23:00:00")) - expect_equal(length(as_timestamp(cf, "timestamp")), 4 * 30 * 24) + t <- CFtime("hours since 2023-01-01", "360_day", "2023-04-30T23:00") + expect_equal(range(t), c("2023-01-01 00:00:00", "2023-04-30 23:00:00")) + expect_equal(length(as_timestamp(t, "timestamp")), 4 * 30 * 24) - expect_error(CFtime("days since 2023-01-01", "366_day", c("2021-01-01", "2021-04-13"))) - cf <- CFtime("days since 2023-01-01", "366_day", c("2023-01-01", "2023-04-13", "2023-10-30", "2023-05-12")) - expect_equal(range(cf), c("2023-01-01", "2023-10-30")) + expect_warning(t <- CFtime("days since 2023-01-01", "366_day", c("2023-01-01", "2023-04-13", "2023-10-30", "2023-05-12"))) + expect_equal(range(t), c("2023-01-01", "2023-10-30")) # Merge two CFtime instances / extend offsets - cf1 <- CFtime("hours since 2001-01-01", "360_day", 0:99) - cf2 <- CFtime("hours since 2001-01-01", "julian", 100:199) - expect_false(cf1 == cf2) - expect_error(cf1 + cf2) - - cf2 <- CFtime("hours since 2001-01-01", "360_day", 100:199) - expect_false(cf1 == cf2) - expect_equal(length(offsets(cf1 + cf2)), 200) - - expect_equal(length(offsets(cf1 + 100:199)), 200) - - cf1 <- CFtime("days since 2022-01-01", "365_day", 0:364) - cf2 <- CFtime("days since 2023-01-01", "365_day", 0:364) - expect_match(capture_output(methods::show(cf1)), "between 365 elements\\)\\n Bounds : not set$") - expect_true(length(offsets(cf1 + cf2)) == 730) - expect_true(all(range(diff(offsets(cf1 + cf2))) == c(1, 1))) - expect_true(length(offsets(cf2 + cf1)) == 730) - expect_false((range(diff(offsets(cf2 + cf1))) == c(1, 1))[1]) + t1 <- CFtime("hours since 2001-01-01", "360_day", 0:99) + t2 <- CFtime("hours since 2001-01-01", "julian", 100:199) + expect_false(t1 == t2) + expect_error(t1 + t2) + + t2 <- CFtime("hours since 2001-01-01", "360_day", 100:199) + expect_false(t1 == t2) + expect_equal(length(offsets(t1 + t2)), 200) + + expect_equal(length(offsets(t1 + 100:199)), 200) + + t1 <- CFtime("days since 2022-01-01", "365_day", 0:364) + t2 <- CFtime("days since 2023-01-01", "365_day", 0:364) + expect_match(capture_output(t1$print()), "between 365 elements\\)\\n Bounds : not set$") + expect_true(length(offsets(t1 + t2)) == 730) + expect_true(all(range(diff(offsets(t1 + t2))) == c(1, 1))) + expect_warning(t3 <- t2 + t1) + expect_true(length(offsets(t3)) == 730) + expect_false((range(diff(offsets(t3))) == c(1, 1))[1]) # Timezones - expect_false(grepl("+0000", capture_output(methods::show(cf1)), fixed = TRUE)) - cf1 <- CFtime("days since 2022-01-01 00:00:00+04", "365_day", 0:364) - expect_true(grepl("+0400", capture_output(methods::show(cf1)), fixed = TRUE)) + expect_false(grepl("+0000", capture_output(t1$print()), fixed = TRUE)) + t1 <- CFtime("days since 2022-01-01 00:00:00+04", "365_day", 0:364) + expect_true(grepl("+0400", capture_output(t1$print()), fixed = TRUE)) # Time series completeness - cf <- CFtime("d per 1991-01-01", "julian") + t <- CFtime("d per 1991-01-01", "julian") expect_error(is_complete("zxcv")) - expect_true(is.na(is_complete(cf))) - expect_true(is_complete(cf1)) + expect_true(is.na(is_complete(t))) + expect_true(is_complete(t1)) mid_months <- c("1950-01-16T12:00:00", "1950-02-15T00:00:00", "1950-03-16T12:00:00", "1950-04-16T00:00:00", "1950-05-16T12:00:00", "1950-06-16T00:00:00", "1950-07-16T12:00:00", "1950-08-16T12:00:00", "1950-09-16T00:00:00", "1950-10-16T12:00:00", "1950-11-16T00:00:00", "1950-12-16T12:00:00") - cf <- CFtime("days since 1950-01-01", "standard", mid_months) - expect_true(is_complete(cf)) - cfy <- CFtime("years since 2020-01-01", "standard", 0:19) - expect_true(is_complete(cfy)) - cfy <- cfy + 30:39 - expect_false(is_complete(cfy)) + t <- CFtime("days since 1950-01-01", "standard", mid_months) + expect_true(is_complete(t)) + ty <- CFtime("years since 2020-01-01", "standard", 0:19) + expect_true(is_complete(ty)) + ty <- ty + 30:39 + expect_false(is_complete(ty)) # Range - cf <- CFtime("days since 2001-01-01") - expect_equal(range(cf), c(NA_character_, NA_character_)) - cf <- cf + 0:1 - expect_error(range(cf, 123)) - expect_error(range(cf, c("asd %d", "%F"))) - expect_equal(range(cf, "%Y-%B-%Od"), c("2001-January-01", "2001-January-02")) + t <- CFtime("days since 2001-01-01") + expect_equal(range(t), c(NA_character_, NA_character_)) + t <- t + 0:1 + expect_error(range(t, 123)) + expect_error(range(t, c("asd %d", "%F"))) + expect_equal(range(t, "%Y-%B-%Od"), c("2001-January-01", "2001-January-02")) # Range on unsorted offsets random <- runif(100, min = 1, max = 99) - cf <- CFtime("days since 2001-01-01", offsets = c(0, random[1:50], 100, random[51:100])) - expect_equal(range(cf), c("2001-01-01", paste0(as.Date("2001-01-01") + 100))) + expect_warning(t <- CFtime("days since 2001-01-01", offsets = c(0, random[1:50], 100, random[51:100]))) + expect_equal(range(t), c("2001-01-01", paste0(as.Date("2001-01-01") + 100))) # Subsetting - cf <- CFtime("hours since 2023-01-01 00:00:00", "standard", 0:239) + t <- CFtime("hours since 2023-01-01 00:00:00", "standard", 0:239) expect_error(slab("zxcv")) - expect_true(all(slab(cf, c("2023-01-01", "2023-02-01")))) - expect_true(length(which(slab(cf, c("2023-01-01", "2023-05-01")))) == 240) - expect_true(length(which(slab(cf, c("2023-01-01 00:00", "2023-01-01 04:00")))) == 4) - expect_true(length(which(slab(cf, c("2023-01-01 04:00", "2023-01-01 00:00")))) == 4) # extremes in reverse order - expect_true(slab(cf, c("2022-01-01", "2023-01-02"))[1]) # early extreme before timeseries - expect_true(all(!slab(cf, c("2023-02-01", "2023-03-01")))) # both extremes outside time series - expect_error(slab(cf, c("2023-01-01 00:00", "2023-01-01 04:00", "2023-01-02 00:00"))) # 3 extremes + expect_true(all(slab(t, c("2023-01-01", "2023-02-01")))) + expect_true(length(which(slab(t, c("2023-01-01", "2023-05-01")))) == 240) + expect_true(length(which(slab(t, c("2023-01-01 00:00", "2023-01-01 04:00")))) == 4) + expect_true(length(which(slab(t, c("2023-01-01 04:00", "2023-01-01 00:00")))) == 4) # extremes in reverse order + expect_true(slab(t, c("2022-01-01", "2023-01-02"))[1]) # early extreme before timeseries + expect_true(all(!slab(t, c("2023-02-01", "2023-03-01")))) # both extremes outside time series + expect_error(slab(t, c("2023-01-01 00:00", "2023-01-01 04:00", "2023-01-02 00:00"))) # 3 extremes }) -test_that("Working with files", { +test_that("Working with packages and files", { lf <- list.files(path = system.file("extdata", package = "CFtime"), full.names = TRUE) - if (!requireNamespace("ncdf4")) return - lapply(lf, function(f) { - nc <- ncdf4::nc_open(lf[1]) - expect_s4_class(CFtime(nc$dim$time$units, nc$dim$time$calendar, nc$dim$time$vals), "CFtime") - ncdf4::nc_close(nc) - }) + if (requireNamespace("ncdf4")) + lapply(lf, function(f) { + nc <- ncdf4::nc_open(f) + expect_s3_class(CFtime(nc$dim$time$units, nc$dim$time$calendar, nc$dim$time$vals), "CFTime") + ncdf4::nc_close(nc) + }) + + # if (requireNamespace("ncdfCF")) + # lapply(lf, function(f) { + # nc <- ncdfCF::open_ncdf(f) + # expect_s3_class(nc[["time"]]$values, "CFTime") + # }) }) diff --git a/tests/testthat/test-parse_deparse.R b/tests/testthat/test-parse_deparse.R index 2bd47b6..52bd185 100644 --- a/tests/testthat/test-parse_deparse.R +++ b/tests/testthat/test-parse_deparse.R @@ -1,33 +1,34 @@ test_that("timestamp string parsing to offsets and deparsing of offsets to timestamps", { - # This test tests: global CFt* constants, CFtime(), CFparse(), as_timestamp(): + # This test tests: global CFt* constants, CFtime(), parse_timestamps(), as_timestamp(): # decomposing offsets into timestamp elements, generating timestamp strings, - # parsing timestamp strings back into timestamp elements. - for (c in CFt$calendars$name) { + # parsing timestamp strings back into timestamp elements, including negative + # offsets. + for (c in c("standard", "gregorian", "proleptic_gregorian", "julian", "360_day", "365_day", "366_day", "noleap", "all_leap")) { for (u in 1:4) { - offsets <- 1:10000 - def <- paste(CFt$units$name[u], "since 1953-08-20") - cf <- CFtime(def, c, offsets) - time <- .offsets2time(cf@offsets, cf@datum) - ts <- as_timestamp(cf, "timestamp") + offsets <- -1000:1000 + def <- paste(CFt$units$name[u], "since 1952-08-20") + t <- CFtime(def, c, offsets) + time <- t$cal$offsets2time(t$offsets) + ts <- as_timestamp(t, "timestamp") cf2 <- CFtime(def, c) - tp <- CFparse(cf2, ts) + tp <- parse_timestamps(cf2, ts) expect_equal(tp, time) } } }) test_that("testing calendars with leap years", { - # This test tests that for standard and julian calendars datums in leap - # years before/on/after the leap day function as needed. Also testing year - # 2000 and 2100 offsets. - for (c in c("standard", "julian")) { + # This test tests that for standard, proleptic_gregorian and julian calendars + # in leap years before/on/after the leap day function as needed. Also testing + # year 2000 and 2100 offsets. + for (c in c("standard", "proleptic_gregorian", "julian")) { for (d in c("1996-01-15", "1996-02-29", "1996-04-01")) { def <- paste("days since", d) - cf <- CFtime(def, c, c(1:2500, 36501:39000)) - time <- .offsets2time(cf@offsets, cf@datum) - ts <- as_timestamp(cf, "timestamp") + t <- CFtime(def, c, c(1:2500, 36501:39000)) + time <- t$cal$offsets2time(t$offsets) + ts <- as_timestamp(t, "timestamp") cf2 <- CFtime(def, c) - tp <- CFparse(cf2, ts) + tp <- parse_timestamps(cf2, ts) expect_equal(tp, time) } } @@ -35,50 +36,48 @@ test_that("testing calendars with leap years", { test_that("Testing milli-second timestamp string parsing to offsets and deparsing of offsets to timestamps", { - # This test tests: global CFt* constants, CFtime(), CFparse(), as_timestamp(): - # decomposing offsets into milli-second timestamp elements, generating timestamp strings, - # parsing timestamp strings back into timestamp elements. - for (c in CFt$calendars$name) { + # This test tests: global CFt* constants, CFtime(), parse_timestamps(), as_timestamp(): + # decomposing offsets into milli-second timestamp elements, generating + # timestamp strings, parsing timestamp strings back into timestamp elements. + for (c in c("standard", "gregorian", "proleptic_gregorian", "julian", "360_day", "365_day", "366_day", "noleap", "all_leap")) { for (u in 1:2) { offsets <- runif(10000, max = 10000) def <- paste(CFt$units$name[u], "since 1953-08-20 07:34:12.2") - cf <- CFtime(def, c, offsets) - time <- .offsets2time(cf@offsets, cf@datum) - ts <- as_timestamp(cf, "timestamp") + expect_warning(t <- CFtime(def, c, offsets)) # not ordered + time <- t$cal$offsets2time(t$offsets) + ts <- as_timestamp(t, "timestamp") cf2 <- CFtime(def, c) - tp <- CFparse(cf2, ts) + tp <- parse_timestamps(cf2, ts) expect_equal(tp[1:6], time[1:6]) } } }) -test_that("Disallow parsing of timestamps on month and year datums", { - for (c in CFt$calendars$name) { +test_that("Disallow parsing of timestamps on month and year calendar units", { + for (c in c("standard", "gregorian", "proleptic_gregorian", "julian", "360_day", "365_day", "366_day", "noleap", "all_leap")) { for (u in 5:6) { def <- paste(CFt$units$name[u], "since 2020-01-01") - cf <- CFtime(def, c, 0:23) - ts <- as_timestamp(cf, "timestamp") - expect_error(CFparse(cf, ts)) + t <- CFtime(def, c, 0:23) + ts <- as_timestamp(t, "timestamp") + expect_error(parse_timestamps(t, ts)) } } }) -test_that("Things can go wrong too", { - expect_false(.is_valid_calendar_date(0, 0, 0, 1)) # Bad year - expect_false(.is_valid_calendar_date(5, 13, 0, 1)) # Bad month - expect_true(.is_valid_calendar_date(5, 10, NA, 1)) # No day so ok - expect_false(.is_valid_calendar_date(5, 10, 0, 1)) # Bad day - expect_false(.is_valid_calendar_date(5, 2, 31, 1)) # No Feb 31 - expect_false(.is_valid_calendar_date(5, 2, 29, 4)) # noleap +test_that("Gregorian/Julian calendar gap in the standard calendar", { + t <- CFtime("days since 1582-10-01", "standard", 0:10) + ts <- as_timestamp(t) + expect_equal(ts[4], "1582-10-04") + expect_equal(ts[5], "1582-10-15") + expect_equal(ts[11], "1582-10-21") + expect_equal(parse_timestamps(t, c("1582-09-30", ts))$offset, -1:10) - years <- c(1900, 2000, 2001, 2002, 2003, 2004, 2100) - expect_equal(.is_leap_year(years, 1), c(F, T, F, F, F, T, F)) - expect_equal(.is_leap_year(years, 2), c(T, T, F, F, F, T, T)) - expect_equal(.is_leap_year(years, 3), c(F, F, F, F, F, F, F)) - expect_equal(.is_leap_year(years, 4), c(F, F, F, F, F, F, F)) - expect_equal(.is_leap_year(years, 5), c(T, T, T, T, T, T, T)) + t <- CFtime("days since 1582-10-20", "standard", -10:0) + ts <- as_timestamp(t) + expect_equal(ts[5], "1582-10-04") + expect_equal(ts[6], "1582-10-15") - cf <- CFtime("days since 0001-01-01", "proleptic_gregorian") - suppressWarnings(expect_warning(CFparse(cf, c("12-1-23", "today")))) # Suppress timezone warning - expect_warning(CFparse(cf, c("2022-08-16T11:07:34.45-10", "2022-08-16 10.5+04"))) + expect_true(t$cal$POSIX_compatible(0:100)) + expect_true(t$cal$POSIX_compatible(-5:100)) + expect_false(t$cal$POSIX_compatible(-6:100)) }) diff --git a/vignettes/CFtime.Rmd b/vignettes/CFtime.Rmd index a48a015..e7bf715 100644 --- a/vignettes/CFtime.Rmd +++ b/vignettes/CFtime.Rmd @@ -38,32 +38,41 @@ On the flip side, the CF Metadata Conventions needs to cater to a wide range of modeling requirements and that means that some of the areas covered by the standards are more complex than might be assumed. One of those areas is the temporal dimension of the data sets. The CF Metadata Conventions supports no less than nine different -calendar definitions, that, upon analysis, fall into five distinct calendars +calendar definitions, that, upon analysis, fall into six distinct calendars (from the perspective of computation of climate projections): - - `standard` or `gregorian`: The international civil calendar that is in common use in + - `standard` (or `gregorian`): The Gregorian calendar that is in common use in many countries around the world, adopted by edict of Pope Gregory XIII in 1582 - and in effect from 15 October of that year. The `proleptic_gregorian` calendar - is the same as the `gregorian` calendar, but with validity - extended to periods prior to `1582-10-15`. + and in effect from 15 October of that year. The earliest valid time in this + calendar is 0001-01-01 00:00:00 (1 January of year 1) as year 0 does not exist + and the CF Metadata Conventions require the year to be positive, but noting + that a Julian calendar is used in periods before the Gregorian calendar was + introduced. + - `proleptic_gregorian`: This is the Gregorian calendar with validity + extended to periods prior to `1582-10-15`, including a year 0 and negative years. + This calendar is being used in most OSes and is what is being used by R. - `julian`: Adopted in the year 45 BCE, every fourth year is a leap year. - Originally, the julian calendar did not have a monotonically increasing year - assigned to it and there are indeed several julian calendars in use around the + Originally, the Julian calendar did not have a monotonically increasing year + assigned to it and there are indeed several Julian calendars in use around the world today with different years assigned to them. Common interpretation - is currently that the year is the same as that of the standard calendar. The - julian calendar is currently 13 days behind the gregorian calendar. - - `365_day` or `noleap`: No years have a leap day. - - `366_day` or `all_leap`: All years have a leap day. - - `360_day`: Every year has 12 months of 30 days each. + is currently that the year is the same as that of the Gregorian calendar. The + Julian calendar is currently 13 days behind the Gregorian calendar. As with the + standard calendar, the earliest valid time in this calendar is 0001-01-01 00:00:00. + - `365_day` or `noleap`: No years have a leap day. Negative years are allowed + and year 0 exists. + - `366_day` or `all_leap`: All years have a leap day. Negative years are allowed + and year 0 exists. + - `360_day`: Every year has 12 months of 30 days each. Negative years are allowed + and year 0 exists. The three latter calendars are specific to the CF Metadata Conventions to reduce computational complexities of working with dates. These three, and the julian calendar, are not compliant with the standard `POSIXt` date/time facilities in `R` and using standard date/time procedures would quickly lead to -problems. In the below code snippet, the date of `1949-12-01` is the *datum* -from which other dates are calculated. When adding 43,289 days to this *datum* +problems. In the below code snippet, the date of `1949-12-01` is the *origin* +from which other dates are calculated. When adding 43,289 days to this origin for a data set that uses the `360_day` calendar, that should yield a date some 120 -years after the *datum*: +years after the origin: ```{r} # POSIXt calculations on a standard calendar - INCORRECT @@ -76,7 +85,7 @@ as_timestamp(CFtime("days since 1949-12-01", "360_day", 43289)) Using standard `POSIXt` calculations gives a result that is about 21 months off from the correct date - obviously an undesirable situation. This example is far -from artificial: `1949-12-01` is the datum for all CORDEX data, covering the +from artificial: `1949-12-01` is the origin for all CORDEX data, covering the period 1951 - 2005 for historical experiments and the period 2006 - 2100 for RCP experiments (with some deviation between data sets), and several models used in the CORDEX set use the `360_day` calendar. The `365_day` or `noleap` calendar @@ -93,38 +102,42 @@ processing of the data. The character of CF time series - a number of numerical offsets from a base date - implies that there should only be a single time zone associated with the time -series. The time zone offset from UTC is stored in the datum and can be retrieved -with the `timezone()` function. If a vector of character timestamps with time +series, and then only for the `standard` and `proleptic_gregorian` calendars. +For the other calendars a time zone can be set but it will have no effect. +Daylight savings time information is never considered by CFtime so the user +should take care to avoid entering times with DST. + +The time zone offset from UTC is stored in the `CFTime` instance and can be +retrieved with the `timezone()` function. If a vector of character timestamps with time zone information is parsed with the `CFparse()` function and the time zones are -found to be different from the datum time zone, a warning message is generated -but the timestamp is interpreted as being in the datum time zone. No correction -of timestamp to datum time zone is performed. +found to be different from the `CFTime` time zone, a warning message is generated +but the timestamp is interpreted as being in the `CFTime` time zone. No correction +of timestamp to `CFTime` time zone is performed. ## Using CFtime to deal with calendars Data sets that are compliant with the CF Metadata Conventions always include a -*datum*, a specific point in time in reference to a specified *calendar*, from +*origin*, a specific point in time in reference to a specified *calendar*, from which other points in time are calculated by adding a specified *offset* of a -certain *unit*. This approach is encapsulated in the `CFtime` package by the S4 -class `CFtime`. +certain *unit*. This approach is encapsulated in the `CFtime` package by the R6 +class `CFTime`. ```{r} -# Create a CF time object from a definition string, a calendar and some offsets -cf <- CFtime("days since 1949-12-01", "360_day", 19830:90029) -cf +# Create a CFTime object from a definition string, a calendar and some offsets +(t <- CFtime("days since 1949-12-01", "360_day", 19830:90029)) ``` -The `CFtime()` function takes a *datum* description (which is actually a unit - -"days" - in reference to a datum - "1949-12-01"), a calendar description, and a -vector of *offsets* from that datum. Once a `CFtime` instance is created its -datum and calendar cannot be changed anymore. Offsets may be added. +The `CFtime()` function takes a description (which is actually a unit - +"days" - in reference to an origin - "1949-12-01"), a calendar description, and a +vector of *offsets* from that origin Once a `CFTime` instance is created its +origin and calendar cannot be changed anymore. Offsets may be added. In practice, these parameters will be taken from the data set of interest. CF Metadata -Conventions require data sets to be in the NetCDF format, with all metadata +Conventions require data sets to be in the netCDF format, with all metadata describing the data set included in a single file, including the mandatory "Conventions" global attribute which should have a string identifying the version of the CF Metadata Conventions that this file adheres to (among possible others). -Not surprisingly, all the pieces of interest are contained in the mandatory `time` +Not surprisingly, all the pieces of interest are contained in the mandatory "time" dimension of the file. The process then becomes as follows, for a CMIP6 file of daily precipitation: @@ -140,43 +153,41 @@ attrs$title attrs$Conventions # Create the CFtime instance from the metadata in the file. -cf <- CFtime(nc$dim$time$units, +(t <- CFtime(nc$dim$time$units, nc$dim$time$calendar, - nc$dim$time$vals) -cf + nc$dim$time$vals)) ``` You can see from the global attribute "Conventions" that the file adheres to the CF Metadata Conventions, among others. According to the CF conventions, `units` -and `calendar` are required attributes of the `time` dimension in the NetCDF file, +and `calendar` are required attributes of the `time` dimension in the netCDF file, and `nc$dim$time$vals` are the offset values, or `dimnames()` in `R` terms, for the `time` dimension of the data. The above example (and others in this vignette) use the `ncdf4` package. If you are using the `RNetCDF` package, checking for CF conventions and then creating a -`CFtime` instance goes like this: +`CFTime` instance goes like this: ```{r} library(RNetCDF) nc <- open.nc(fn) att.get.nc(nc, -1, "Conventions") -cf <- CFtime(att.get.nc(nc, "time", "units"), +(t <- CFtime(att.get.nc(nc, "time", "units"), att.get.nc(nc, "time", "calendar"), - var.get.nc(nc, "time")) -cf + var.get.nc(nc, "time"))) ``` The corresponding character representations of the time series can be easily generated: ```{r} -dates <- as_timestamp(cf, format = "date") +dates <- as_timestamp(t, format = "date") dates[1:10] ``` -...as well as the full range of the time series: +...as well as the range of the time series: ```{r} -range(cf) +range(t) ``` Note that in this latter case, if any of the timestamps in the time series have a time that is @@ -194,33 +205,33 @@ season, and then compute a derivative value such as the dekadal sum of precipita monthly minimum/maximum daily temperature, or seasonal average daily short-wave irradiance. -It is also possible to create factors for multiple "epochs" in one go. This greatly +It is also possible to create factors for multiple "eras" in one go. This greatly reduces programming effort if you want to calculate anomalies over multiple future periods. A complete example is provided in the vignette ["Processing climate projection data"](Processing.html). -It is easy to generate the factors that you need once you have a `CFtime` instance +It is easy to generate the factors that you need once you have a `CFTime` instance prepared: ```{r} # Create a dekad factor for the whole `cf` time series that was created above -f_k <- CFfactor(cf, "dekad") +f_k <- CFfactor(t, "dekad") str(f_k) -# Create monthly factors for a baseline epoch and early, mid and late 21st century epochs -baseline <- CFfactor(cf, epoch = 1991:2020) -future <- CFfactor(cf, epoch = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) +# Create monthly factors for a baseline era and early, mid and late 21st century eras +baseline <- CFfactor(t, era = 1991:2020) +future <- CFfactor(t, era = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) str(future) ``` -For the "epoch" version, there are two interesting things to note here: +For the "era" version, there are two interesting things to note here: - - The epochs do not have to coincide with the boundaries of the time series. In the - example above, the time series starts in 2015, while the baseline epoch is from 1991. + - The eras do not have to coincide with the boundaries of the time series. In the + example above, the time series starts in 2015, while the baseline era is from 1991. Obviously, the number of time steps from the time series that then fall within - this epoch will then be reduced. + this era will then be reduced. - The factor is always of the same length as the time series, with `NA` values - where the time series values are not falling in the epoch. This ensures that the + where the time series values are not falling in the era. This ensures that the factor is compatible with the data set which the time series describes, such that functions like `tapply()` will not throw an error. @@ -238,16 +249,15 @@ There are six periods defined for `CFfactor()`: ##### New "time" dimension -A CFtime instance describes the "time" dimension of an associated data set. When +A `CFTime` instance describes the "time" dimension of an associated data set. When you process that dimension of the data set using `CFfactor()` or another method to filter or otherwise subset the "time" dimension, the resulting data set will -have a different "time" dimension. To associate a proper CFtime instance with -your processing result, the methods in this package return that CFtime instance +have a different "time" dimension. To associate a proper `CFTime` instance with +your processing result, the methods in this package return that `CFTime` instance as an attribute: ```{r} - new_time <- attr(f_k, "CFtime") - new_time + (new_time <- attr(f_k, "CFTime")) ``` In the vignette ["Processing climate projection data"](Processing.html) is a @@ -269,17 +279,17 @@ processing or apply weights based on the actual coverage. ```{r} # Is the time series complete? -is_complete(cf) +is_complete(t) # How many time units fit in a factor level? -CFfactor_units(cf, baseline) +CFfactor_units(t, baseline) # What's the absolute and relative coverage of our time series -CFfactor_coverage(cf, baseline, "absolute") -CFfactor_coverage(cf, baseline, "relative") +CFfactor_coverage(t, baseline, "absolute") +CFfactor_coverage(t, baseline, "relative") ``` -The time series is complete but coverage of the baseline epoch is only +The time series is complete but coverage of the baseline era is only 20%! Recall that the time series starts in 2015 while the baseline period in the factor is for `1991:2020` so that's only 6 years of time series data out of 30 years of the baseline factor. @@ -292,13 +302,12 @@ n <- 365 * 4 cov <- 0.8 offsets <- sample(0:(n-1), n * cov) -cf <- CFtime("days since 2020-01-01", "365_day", offsets) -cf +(t <- CFtime("days since 2020-01-01", "365_day", offsets)) # Note that there are about 1.25 days between observations -mon <- CFfactor(cf, "month") -CFfactor_coverage(cf, mon, "absolute") -CFfactor_coverage(cf, mon, "relative") +mon <- CFfactor(t, "month") +CFfactor_coverage(t, mon, "absolute") +CFfactor_coverage(t, mon, "relative") ``` Keep in mind, though, that there are data sets where the time unit is lower than @@ -311,8 +320,9 @@ set is complete with the function `CFcomplete()`. ## CFtime and POSIXt The CF Metadata Conventions support nine different calendars. Of these, only the -`standard`, `gregorian` and `proleptic_gregorian` calendars are fully compatible -with POSIXt. The other calendars have varying degrees of discrepancies: +`proleptic_gregorian` calendar is fully compatible with POSIXt, with the +`standard` and `gregorian` calendars only valid for periods after 1582-10-15. The +other calendars have varying degrees of discrepancies: - `julian`: Every fouth year is a leap year. Dates like `2100-02-29` and `2200-02-29` are valid. @@ -327,17 +337,17 @@ to produce problems. This is most pronounced for the `360_day` calendar: ```{r} # Days in January and February -cf <- CFtime("days since 2023-01-01", "360_day", 0:59) -cf_days <- as_timestamp(cf, "date") -as.Date(cf_days) +t <- CFtime("days since 2023-01-01", "360_day", 0:59) +ts_days <- as_timestamp(t, "date") +as.Date(ts_days) ``` 31 January is missing from the vector of `Date`s because the `360_day` calendar does not include it and 29 and 30 February are `NA`s because POSIXt rejects them. This will produce problems later on when processing your data. -The general advice is therefore: **do not convert CFtime objects to Date objects** -unless you are sure that the `CFtime` object uses a POSIXt-compatible calendar. +The general advice is therefore: **do not convert CFTime objects to Date objects** +unless you are sure that the `CFTime` object uses a POSIXt-compatible calendar. ##### So how do I compare climate projection data with different calendars? @@ -360,7 +370,7 @@ Otherwise, there really shouldn't be any reason to convert the time series in th data files to `Date`s. Climate projection data is virtually never compared on a day-to-day basis between different models and neither does complex date arithmetic make much sense (such as adding intervals) - `CFtime` can support basic arithmetic -by manipulation the offsets of the `CFtime` object. The character representations +by manipulation the offsets of the `CFTime` object. The character representations that are produced are perfectly fine to use for `dimnames()` on an array or as `rownames()` in a `data.frame` and these also support basic logical operations such as `"2023-02-30" < "2023-03-01"`. So ask yourself, do you really need `Date`s diff --git a/vignettes/Processing.Rmd b/vignettes/Processing.Rmd index 0ece341..4937d29 100644 --- a/vignettes/Processing.Rmd +++ b/vignettes/Processing.Rmd @@ -62,10 +62,10 @@ data files. The function comes in two operating modes: - Plain vanilla mode produces a factor for a time period across the entire time series. The factor level includes the year. This would be useful to calculate mean temperature for every month in every year, for instance. - - When one or more "epochs" (periods of interest) are provided, the factor level + - When one or more "eras" (periods of interest) are provided, the factor level no longer includes the year and can be used to calculate, for instance, the mean - temperature per period of interest in the epoch (e.g. average March temperature - in the epoch 2041-2060). + temperature per period of interest in the era (e.g. average March temperature + in the era 2041-2060). ```{r} # Setting up @@ -75,9 +75,9 @@ cf <- CFtime(nc$dim$time$units, nc$dim$time$calendar, nc$dim$time$vals) -# Create monthly factors for a baseline epoch and early, mid and late 21st century epochs -baseline <- CFfactor(cf, epoch = 1991:2020) -future <- CFfactor(cf, epoch = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) +# Create monthly factors for a baseline era and early, mid and late 21st century eras +baseline <- CFfactor(cf, era = 1991:2020) +future <- CFfactor(cf, era = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) str(baseline) str(future) ``` @@ -88,7 +88,7 @@ processing of the data into precipitation anomalies for 3 periods relative to a baseline period could look like this: ```{r} -# Read the data from the NetCDF file. +# Read the data from the netCDF file. # Keep degenerate dimensions so that we have a predictable data structure: 3-dimensional array. # Converts units of kg m-2 s-1 to mm/day. pr <- ncvar_get(nc, "pr", collapse_degen = FALSE) * 86400 @@ -102,11 +102,11 @@ experiment <- ncatt_get(nc, "")$experiment_id nc_close(nc) # Calculate the daily average precipitation per month for the baseline period -# and the three future epochs. +# and the three future eras. pr_base <- apply(pr, 1:2, tapply, baseline, mean) # an array pr_future <- lapply(future, function(f) apply(pr, 1:2, tapply, f, mean)) # a list of arrays -# Calculate the precipitation anomalies for the future epochs against the baseline. +# Calculate the precipitation anomalies for the future eras against the baseline. # Working with daily averages per month so we can simply subtract and then multiply by days # per month for each of the factor levels using the CF calendar. ano <- mapply(function(pr, f) {(pr - pr_base) * CFfactor_units(cf, f)}, pr_future, future, SIMPLIFY = FALSE) @@ -121,7 +121,7 @@ lines(1:12, ano$late[,1,1], type = "o", col = "red") Looks like Hadley will be needing rubber boots in spring and autumn back home! -The interesting feature, working from opening the NetCDF file down to plotting, is that +The interesting feature, working from opening the netCDF file down to plotting, is that the specifics of the CF calendar that the data suite uses do not have to be considered anywhere in the processing workflow: the `CFtime` package provides the functionality. Data suites using another CF calendar are processed exactly the same. @@ -157,23 +157,23 @@ ano <- lapply(lf, function(fn) { pr <- ncvar_get(nc, "pr", collapse_degen = FALSE) * 86400 nc_close(nc) - baseline <- CFfactor(cf, epoch = 1991:2020) + baseline <- CFfactor(cf, era = 1991:2020) pr_base <- apply(pr, 1:2, tapply, baseline, mean) - future <- CFfactor(cf, epoch = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) + future <- CFfactor(cf, era = list(early = 2021:2040, mid = 2041:2060, late = 2061:2080)) pr_future <- lapply(future, function(f) apply(pr, 1:2, tapply, f, mean)) mapply(function(pr, f) {(pr - pr_base) * CFfactor_units(cf, f)}, pr_future, future, SIMPLIFY = FALSE) }) -# Epoch names -epochs <- c("early", "mid", "late") -dim(epochs) <- 3 +# Era names +eras <- c("early", "mid", "late") +dim(eras) <- 3 -# Build the ensemble for each epoch -# For each epoch, grab the data for each of the ensemble members, simplify to an array +# Build the ensemble for each era +# For each era, grab the data for each of the ensemble members, simplify to an array # and take the mean per row (months, in this case) -ensemble <- apply(epochs, 1, function(e) { +ensemble <- apply(eras, 1, function(e) { rowMeans(sapply(ano, function(a) a[[e]], simplify = T))}) -colnames(ensemble) <- epochs +colnames(ensemble) <- eras rownames(ensemble) <- rownames(ano[[1]][[1]]) ensemble ``` @@ -241,7 +241,7 @@ prepare_CORDEX <- function(fn, var) { ``` Calling this function like `prepare_CORDEX(list.files(path = "~/CC/CORDEX/CAM", -pattern = "^pr.*\\.nc$", full.names = TRUE), "pr")` will yield a list of NetCDF +pattern = "^pr.*\\.nc$", full.names = TRUE), "pr")` will yield a list of netCDF files with precipitation data, with the resulting `CFtime` instance describing the full temporal extent covered by the data files, as well as the data bound on the temporal dimension, ready for further processing.