Skip to content

Commit ed0d98b

Browse files
author
maechler
committed
fix format.POSIXlt() for fractional secs
git-svn-id: https://svn.r-project.org/R/trunk@87354 00db46b3-68df-0310-9c12-caf00c1e9a41
1 parent 56ae399 commit ed0d98b

File tree

4 files changed

+71
-19
lines changed

4 files changed

+71
-19
lines changed

doc/NEWS.Rd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,12 @@
368368
\item \code{debugonce(<S4-simple-body>, signature=*)} now works
369369
correctly when \dQuote{called twice}, fixing \PR{18824} thanks to
370370
\I{Michael Jagan}.
371+
372+
\item \code{format(dtime, digits=* / format=*)} is more consistent
373+
when the \code{POSIXt} date-time object \code{dtime} has fractional
374+
(non integer) seconds. Fixes \PR{17350}, thanks to new contributions
375+
by \I{LatinR}'s \sQuote{\I{R Dev Day}} participants, \I{Heather
376+
Turner} and \I{Dirk Eddelbuettel}.
371377
}
372378
}
373379
}

src/library/base/R/datetime.R

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -382,18 +382,20 @@ format.POSIXlt <- function(x, format = "", usetz = FALSE,
382382
digits = getOption("digits.secs"), ...)
383383
{
384384
if(!inherits(x, "POSIXlt")) stop("wrong class")
385-
if(any(f0 <- format == "")) {
386-
## need list [ method here.
387-
times <- unlist(unclass(x)[1L:3L])[f0]
388-
secs <- x$sec[f0]; secs <- secs[is.finite(secs)]
389-
np <- if(is.null(digits)) 0L else min(6L, digits)
390-
if(np >= 1L) # no unnecessary trailing '0' :
391-
for (i in seq_len(np)- 1L)
392-
if(all( abs(secs - round(secs, i)) < 1e-6 )) {
385+
if(any(f0 <- format == "" | grepl("%OS$", format))) {
386+
if(!is.null(digits)) {
387+
secs <- x$sec[f0]; secs <- secs[is.finite(secs)]
388+
np <- min(6L, digits)
389+
## no unnecessary trailing '0' ; use trunc() as .Internal() code:
390+
for(i in seq_len(np)- 1L)
391+
if(all( abs(secs - trunc(secs*(ti <- 10^i))/ti) < 1e-6 )) {
393392
np <- i
394393
break
395394
}
396-
format[f0] <-
395+
} else np <- 0L
396+
## need list `[` method here to get 1:3 ~ {sec, min, hour}:
397+
times <- unlist(`names<-`(unclass(x)[1L:3L], NULL))[f0]
398+
format[f0] <-
397399
if(all(times[is.finite(times)] == 0)) "%Y-%m-%d"
398400
else if(np == 0L) "%Y-%m-%d %H:%M:%S"
399401
else paste0("%Y-%m-%d %H:%M:%OS", np)

src/library/base/man/strptime.Rd

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ strptime(x, format, tz = "")
3939
methods is
4040
\code{"\%Y-\%m-\%d \%H:\%M:\%S"} if any element has a time
4141
component which is not midnight, and \code{"\%Y-\%m-\%d"}
42-
otherwise. If \code{digits} is not \code{NULL}, i.e., by default when
43-
\code{\link{options}("digits.secs")} is set, up to
44-
the specified number of digits will be printed for seconds.}
42+
otherwise. In the first case and if \code{digits} is not \code{NULL},
43+
i.e., by default when \code{\link{options}("digits.secs")} is set, up to
44+
the specified number of digits will be printed for seconds, using
45+
\code{"\%OS<n>"} instead of \code{"\%S"} in the format, see also
46+
\sQuote{Details}.}
4547
\item{\dots}{further arguments to be passed from or to other methods.}
4648
\item{usetz}{logical. Should the time zone abbreviation be appended
4749
to the output? This is used in printing times, and more reliable
@@ -237,7 +239,9 @@ strptime(x, format, tz = "")
237239
Specific to \R is \code{\%OSn}, which for output gives the seconds
238240
truncated to \code{0 <= n <= 6} decimal places (and if \code{\%OS} is
239241
not followed by a digit, it uses \code{digits} unless that is
240-
\code{NULL}, when \code{n = 0}). Further, for \code{strptime}
242+
\code{NULL}, when \code{n = 0}). Note that the precedence is
243+
\code{format="...\%OSn"} \eqn{\ll}{>>} \code{digits = n} \eqn{\ll}{>>}
244+
\code{getOption("digits.prec")}. Further, for \code{strptime}
241245
\code{\%OS} will input seconds including fractional seconds. Note that
242246
\code{\%S} does not read fractional parts on output.
243247
@@ -424,14 +428,17 @@ z2 <- strptime(x, "\%m/\%d/\%y \%H:\%M:\%S")
424428
## *here* (but not in general), the same as format():
425429
stopifnot(identical(format(z2), as.character(z2)))
426430
427-
## time with fractional seconds
428-
z3 <- strptime("20/2/06 11:16:16.683", "\%d/\%m/\%y \%H:\%M:\%OS") \donttest{
431+
## time with fractional seconds (setting `tz = ..` for reproducible output)
432+
z3 <- strptime("20/2/06 11:16:16.683", "\%d/\%m/\%y \%H:\%M:\%OS", tz = "UTC")
429433
z3 # prints without fractional seconds by default, digits.sec = NULL ("= 0")
430-
print(z3, digits = 3) # shows extra digits}
431-
op <- options(digits.secs = 3)
432-
\donttest{z3 # shows the 3 extra digits}
433-
as.character(z3) # ditto
434+
format(z3, digits = 3) # shows extra digits
435+
format(z3, digits = 6) # still 3 digits: *not* showing trailing zeros
436+
format(z3, format = "\%Y-\%m-\%d \%H:\%M:\%OS6") # *does* keep trailing zeros
437+
op <- options(digits.secs = 3) # global option, the default for `digits`
438+
z3 # shows the 3 extra digits
434439
options(op)
440+
as.character(z3) # ditto
441+
435442
436443
## time zone names are not portable, but 'EST5EDT' comes pretty close.
437444
## (but its interpretation may not be universal: see ?timezones)

tests/datetime5.R

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,40 @@ for (f in c("P", "k", "l", "s")) {
2828
dt2 <- as.POSIXlt(sprintf("%d-01-01 09:03;04", 2015:2018))
2929
cat(format(dt2, "%Y: %U %V %W"), sep = "\n")
3030

31+
32+
## fractional seconds print(<POSIXct>) --> format.POSIXlt() -- PR#17350 (and rdev day #83)
33+
## Original PR#17350 example (Vitalie Spinu):
34+
op <- options(digits.secs = 6, scipen = 20, digits = 15)
35+
## what we'd desire for print()ing etc:
36+
chx <- paste0("2009-08-03 12:01:59", c("", paste0(".",1:3)))
37+
print(chx, width = 40)
38+
xl <- as.POSIXlt(chx)
39+
stopifnot(identical(xl$sec, 59 + 0:3/10)) # POSIXlt keeping full precision (always did)
40+
## (but all arithmetic with POSIX*t currently happens via POSIXct, losing precision)
41+
fxl <- format(xl) # is perfect {with getOption("digits.secs") > 0 !}
42+
stopifnot(identical(sub(".*:59", '', fxl), paste0(".", 0:3)))
43+
x <- as.POSIXct("2009-08-03 12:01:59") + 0:3/10 # using POSIXct looses prec
44+
x. <- structure(x, tzone = "") ## == Vitalie's explicit original ex.
45+
identical(x, x.) # FALSE : x. contains `tzone = ""`
46+
print(x, width = 40) # now .000000 .099999 2.00000 2.999999 (as digits.secs = 6 !)
47+
fx <- format(x)
48+
stopifnot(identical(fx, format(x.))) # *are* the same (for a while now)
49+
## The %OS and %OS<d> formats have been fine "always":
50+
fD.OS <- function(d) format(x, format = paste0("%Y-%m-%d %H:%M:%OS", if(d=="_") "" else d))
51+
f.OSss <- vapply(c("_",0:6), fD.OS, character(length(x)))
52+
t(f.OSss) |> print(width=111, quote=FALSE) # shows 'trunc()' instead of 'round()'
53+
stopifnot(identical(f.OSss[,"_"], f.OSss[,"6"])) # by option digits.secs
54+
(secDig <- sub(".*:59", '', f.OSss)) ## [,"1"] is *.0 *.0 *.2 *.2 - "bad" from using trunc() by design
55+
## ___________ ___ __ __ "factory fresh" default
56+
options(digits.secs = NULL, scipen = 0, digits = 7)
57+
f.OSssD <- vapply(c("_",0:6), fD.OS, character(length(x))) # same call but different "digits.secs" option
58+
## digits = <d> now works "the same":
59+
fdig <- vapply(c("_",0:6), \(d) format(x, digits = if(d != "_") d), character(length(x)))
60+
stopifnot(exprs = {
61+
nchar(t(secDig)) == c(7L, 0L, 2:7) # as always
62+
identical(f.OSssD[, 1], f.OSssD[,"0"]) # "" <--> "0"
63+
identical(f.OSss [,-1], f.OSssD[, -1]) # only meaning of `empty' "%OS" changes with "digits.secs" option
64+
identical(fdig, f.OSssD)
65+
})
66+
options(op)
67+
## Number of digits used differed in several cases in R <= 4.4.z

0 commit comments

Comments
 (0)