1
Fork 0
mirror of https://git.savannah.gnu.org/git/guile.git synced 2025-05-20 11:40:18 +02:00

SRFI-19: Fix TAI->UTC conversions, leap second handling, etc.

Fixes <https://bugs.gnu.org/21911>.
Fixes <https://bugs.gnu.org/22034>.
Fixes <https://bugs.gnu.org/21902>.
Partially fixes <https://bugs.gnu.org/21904>.
Reported by Zefram <zefram@fysh.org>.

* doc/ref/srfi-modules.texi (SRFI-19 Introduction): Fix the definitions
of Julian Day and Modified Julian Day.  Give the correct full names of
UTC and TAI.
* module/srfi/srfi-19.scm: Import (srfi srfi-1).  Use modern Guile
keyword syntax in the 'define-module' form.
(leap-second-neg-delta): New procedure, derived from a similar procedure
in the latest upstream SRFI-19 reference implementation.
(priv:time-tai->time-utc!, time-tai->julian-day)
(time-monotonic->julian-day): Use 'leap-second-neg-delta'.
(local-tz-offset): Fix comment.
(leap-second?): Remove.
(tai-before-leap-second?): New procedure, derived from upstream SRFI-19.
(time-utc->date): Use 'define*' to handle the optional argument.  Remove
the leap second handling, following upstream SRFI-19.
(time-tai->date): Rewrite in terms of 'time-utc->date'.  Add special
leap second handling, following upstream SRFI-19.
(time-monotonic->date): Rewrite in terms of 'time-tai->date'.
(date->time-tai, date->time-monotonic): Add special leap second
handling, following upstream SRFI-19.
(directives): In the entry for the "~Y" escape in 'date->string', pad
the year field to 4 characters, following upstream SRFI-19.
* test-suite/tests/srfi-19.test: Add tests.
This commit is contained in:
Mark H Weaver 2018-10-20 03:34:56 -04:00 committed by Andy Wingo
parent 4e24cca595
commit e00563492a
3 changed files with 182 additions and 102 deletions

View file

@ -2402,8 +2402,8 @@ functions and variables described here are provided by
@cindex UTC
@cindex TAI
This module implements time and date representations and calculations,
in various time systems, including universal time (UTC) and atomic
time (TAI).
in various time systems, including Coordinated Universal Time (UTC)
and International Atomic Time (TAI).
For those not familiar with these time systems, TAI is based on a
fixed length second derived from oscillations of certain atoms. UTC
@ -2435,18 +2435,14 @@ in @file{srfi-19.scm} for how to update this table.
@cindex julian day
@cindex modified julian day
Also, for those not familiar with the terminology, a @dfn{Julian Day}
is a real number which is a count of days and fraction of a day, in
UTC, starting from -4713-01-01T12:00:00Z, ie.@: midday Monday 1 Jan
4713 B.C. A @dfn{Modified Julian Day} is the same, but starting from
1858-11-17T00:00:00Z, ie.@: midnight 17 November 1858 UTC. That time
is julian day 2400000.5.
represents a point in time as a real number of days since
-4713-11-24T12:00:00Z, i.e.@: midday UT on 24 November 4714 BC in the
proleptic Gregorian calendar (1 January 4713 BC in the proleptic Julian
calendar).
@c The SRFI-1 spec says -4714-11-24T12:00:00Z (November 24, -4714 at
@c noon, UTC), but this is incorrect. It looks like it might have
@c arisen from the code incorrectly treating years a multiple of 100
@c but not 400 prior to 1582 as non-leap years, where instead the Julian
@c calendar should be used so all multiples of 4 before 1582 are leap
@c years.
A @dfn{Modified Julian Day} represents a point in time as a real number
of days since 1858-11-17T00:00:00Z, i.e.@: midnight UT on Wednesday 17
November AD 1858. That time is julian day 2400000.5.
@node SRFI-19 Time

View file

@ -40,13 +40,14 @@
;; the DATE structure.
(define-module (srfi srfi-19)
:use-module (srfi srfi-6)
:use-module (srfi srfi-8)
:use-module (srfi srfi-9)
:autoload (ice-9 rdelim) (read-line)
:use-module (ice-9 i18n)
:replace (current-time)
:export (;; Constants
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-6)
#:use-module (srfi srfi-8)
#:use-module (srfi srfi-9)
#:autoload (ice-9 rdelim) (read-line)
#:use-module (ice-9 i18n)
#:replace (current-time)
#:export (;; Constants
time-duration
time-monotonic
time-process
@ -244,6 +245,16 @@
(if (< utc-seconds (* (- 1972 1970) 365 sid)) 0
(lsd leap-second-table))))
;; going from tai seconds to utc seconds ...
(define (leap-second-neg-delta tai-seconds)
(letrec ((lsd (lambda (table)
(cond ((null? table) 0)
((>= tai-seconds (+ (caar table) (cdar table)))
(cdar table))
(else (lsd (cdr table)))))) )
(if (< tai-seconds (* (- 1972 1970) 365 sid)) 0
(lsd leap-second-table))))
;;; the TIME structure; creates the accessors, too.
@ -449,7 +460,7 @@
(set-time-type! time-out time-utc)
(set-time-nanosecond! time-out (time-nanosecond time-in))
(set-time-second! time-out (- (time-second time-in)
(leap-second-delta
(leap-second-neg-delta
(time-second time-in))))
time-out)
@ -594,7 +605,7 @@
;; This should be written to be OS specific.
(define (local-tz-offset utc-time)
;; SRFI uses seconds West, but guile (and libc) use seconds East.
;; SRFI 19 uses seconds East, but 'tm:gmtoff' returns seconds West.
(- (tm:gmtoff (localtime (time-second utc-time)))))
;; special thing -- ignores nanos
@ -603,21 +614,16 @@
sid)
tai-epoch-in-jd))
(define (leap-second? second)
(and (assoc second leap-second-table) #t))
(define (tai-before-leap-second? second)
(any (lambda (x)
(= second (+ (car x) (cdr x) -1)))
leap-second-table))
(define (time-utc->date time . tz-offset)
(define* (time-utc->date time #:optional (tz-offset
(local-tz-offset time)))
(if (not (eq? (time-type time) time-utc))
(time-error 'time-utc->date 'incompatible-time-types time))
(let* ((offset (if (null? tz-offset)
(local-tz-offset time)
(car tz-offset)))
(leap-second? (leap-second? (+ offset (time-second time))))
(jdn (time->julian-day-number (if leap-second?
(- (time-second time) 1)
(time-second time))
offset)))
(let ((jdn (time->julian-day-number (time-second time) tz-offset)))
(call-with-values (lambda () (decode-julian-day-number jdn))
(lambda (secs date month year)
;; secs is a real because jdn is a real in Guile;
@ -628,78 +634,34 @@
(minutes (quotient rem 60))
(seconds (remainder rem 60)))
(make-date (time-nanosecond time)
(if leap-second? (+ seconds 1) seconds)
seconds
minutes
hours
date
month
year
offset))))))
tz-offset))))))
(define (time-tai->date time . tz-offset)
(if (not (eq? (time-type time) time-tai))
(time-error 'time-tai->date 'incompatible-time-types time))
(let* ((offset (if (null? tz-offset)
(local-tz-offset (time-tai->time-utc time))
(car tz-offset)))
(seconds (- (time-second time)
(leap-second-delta (time-second time))))
(leap-second? (leap-second? (+ offset seconds)))
(jdn (time->julian-day-number (if leap-second?
(- seconds 1)
seconds)
offset)))
(call-with-values (lambda () (decode-julian-day-number jdn))
(lambda (secs date month year)
;; secs is a real because jdn is a real in Guile;
;; but it is conceptionally an integer.
;; adjust for leap seconds if necessary ...
(let* ((int-secs (inexact->exact (round secs)))
(hours (quotient int-secs (* 60 60)))
(rem (remainder int-secs (* 60 60)))
(minutes (quotient rem 60))
(seconds (remainder rem 60)))
(make-date (time-nanosecond time)
(if leap-second? (+ seconds 1) seconds)
minutes
hours
date
month
year
offset))))))
(if (tai-before-leap-second? (time-second time))
;; If it's *right* before the leap, we must handle this case to
;; avoid the information lost when converting to UTC. We subtract
;; a second before conversion, and then effectively add it back
;; after conversion by setting the second field to 60.
(let ((d (apply time-utc->date
(subtract-duration! (time-tai->time-utc time)
(make-time time-duration 0 1))
tz-offset)))
(set-date-second! d 60)
d)
(apply time-utc->date (time-tai->time-utc time) tz-offset)))
;; this is the same as time-tai->date.
(define (time-monotonic->date time . tz-offset)
(if (not (eq? (time-type time) time-monotonic))
(time-error 'time-monotonic->date 'incompatible-time-types time))
(let* ((offset (if (null? tz-offset)
(local-tz-offset (time-monotonic->time-utc time))
(car tz-offset)))
(seconds (- (time-second time)
(leap-second-delta (time-second time))))
(leap-second? (leap-second? (+ offset seconds)))
(jdn (time->julian-day-number (if leap-second?
(- seconds 1)
seconds)
offset)))
(call-with-values (lambda () (decode-julian-day-number jdn))
(lambda (secs date month year)
;; secs is a real because jdn is a real in Guile;
;; but it is conceptionally an integer.
;; adjust for leap seconds if necessary ...
(let* ((int-secs (inexact->exact (round secs)))
(hours (quotient int-secs (* 60 60)))
(rem (remainder int-secs (* 60 60)))
(minutes (quotient rem 60))
(seconds (remainder rem 60)))
(make-date (time-nanosecond time)
(if leap-second? (+ seconds 1) seconds)
minutes
hours
date
month
year
offset))))))
(apply time-tai->date (time-monotonic->time-tai time) tz-offset))
(define (date->time-utc date)
(let* ((jdays (- (encode-julian-day-number (date-day date)
@ -717,11 +679,17 @@
(date-second date)
(- (date-zone-offset date))))))
(define (date->time-tai date)
(time-utc->time-tai! (date->time-utc date)))
(define (date->time-tai d)
(if (= (date-second d) 60)
(subtract-duration! (time-utc->time-tai! (date->time-utc d))
(make-time time-duration 0 1))
(time-utc->time-tai! (date->time-utc d))))
(define (date->time-monotonic date)
(time-utc->time-monotonic! (date->time-utc date)))
(define (date->time-monotonic d)
(if (= (date-second d) 60)
(subtract-duration! (time-utc->time-monotonic! (date->time-utc d))
(make-time time-duration 0 1))
(time-utc->time-monotonic! (date->time-utc d))))
(define (leap-year? year)
(or (= (modulo year 400) 0)
@ -835,7 +803,7 @@
(if (not (eq? (time-type time) time-tai))
(time-error 'time-tai->julian-day 'incompatible-time-types time))
(+ (/ (+ (- (time-second time)
(leap-second-delta (time-second time)))
(leap-second-neg-delta (time-second time)))
(/ (time-nanosecond time) nano))
sid)
tai-epoch-in-jd))
@ -849,7 +817,7 @@
(if (not (eq? (time-type time) time-monotonic))
(time-error 'time-monotonic->julian-day 'incompatible-time-types time))
(+ (/ (+ (- (time-second time)
(leap-second-delta (time-second time)))
(leap-second-neg-delta (time-second time)))
(/ (time-nanosecond time) nano))
sid)
tai-epoch-in-jd))
@ -1093,7 +1061,10 @@
2)
port)))
(cons #\Y (lambda (date pad-with port)
(display (date-year date) port)))
(display (padding (date-year date)
pad-with
4)
port)))
(cons #\z (lambda (date pad-with port)
(tz-printer (date-zone-offset date) port)))
(cons #\Z (lambda (date pad-with port)

View file

@ -1,8 +1,8 @@
;;;; srfi-19.test --- test suite for SRFI-19 -*- scheme -*-
;;;; Matthias Koeppe <mkoeppe@mail.math.uni-magdeburg.de> --- June 2001
;;;;
;;;; Copyright (C) 2001, 2003, 2004, 2005, 2006, 2007, 2008,
;;;; 2011, 2014, 2017 Free Software Foundation, Inc.
;;;; Copyright (C) 2001, 2003-2008, 2011, 2014, 2017, 2018
;;;; Free Software Foundation, Inc.
;;;;
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
@ -211,6 +211,9 @@ incomplete numerical tower implementation.)"
(pass-if "31dec98 23:59:59"
(time-equal? (make-time time-tai 0 915148830)
(date->time-tai (make-date 0 59 59 23 31 12 1998 0))))
(pass-if "31dec98 23:59:60"
(time-equal? (make-time time-tai 0 915148831)
(date->time-tai (make-date 0 60 59 23 31 12 1998 0))))
(pass-if "1jan99 0:00:00"
(time-equal? (make-time time-tai 0 915148832)
(date->time-tai (make-date 0 0 0 0 1 1 1999 0))))
@ -220,10 +223,120 @@ incomplete numerical tower implementation.)"
(pass-if "31dec05 23:59:59"
(time-equal? (make-time time-tai 0 1136073631)
(date->time-tai (make-date 0 59 59 23 31 12 2005 0))))
(pass-if "31dec05 23:59:60"
(time-equal? (make-time time-tai 0 1136073632)
(date->time-tai (make-date 0 60 59 23 31 12 2005 0))))
(pass-if "1jan06 0:00:00"
(time-equal? (make-time time-tai 0 1136073633)
(date->time-tai (make-date 0 0 0 0 1 1 2006 0)))))
(with-test-prefix "date->time-monotonic"
;; leap second 1 Jan 1999, 1 second of UTC in make-date is out as 2
;; seconds of MONOTONIC in date->time-monotonic
(pass-if "31dec98 23:59:59"
(time-equal? (make-time time-monotonic 0 915148830)
(date->time-monotonic (make-date 0 59 59 23 31 12 1998 0))))
(pass-if "31dec98 23:59:60"
(time-equal? (make-time time-monotonic 0 915148831)
(date->time-monotonic (make-date 0 60 59 23 31 12 1998 0))))
(pass-if "1jan99 0:00:00"
(time-equal? (make-time time-monotonic 0 915148832)
(date->time-monotonic (make-date 0 0 0 0 1 1 1999 0))))
;; leap second 1 Jan 2006, 1 second of UTC in make-date is out as 2
;; seconds of MONOTONIC in date->time-monotonic
(pass-if "31dec05 23:59:59"
(time-equal? (make-time time-monotonic 0 1136073631)
(date->time-monotonic (make-date 0 59 59 23 31 12 2005 0))))
(pass-if "31dec05 23:59:60"
(time-equal? (make-time time-monotonic 0 1136073632)
(date->time-monotonic (make-date 0 60 59 23 31 12 2005 0))))
(pass-if "1jan06 0:00:00"
(time-equal? (make-time time-monotonic 0 1136073633)
(date->time-monotonic (make-date 0 0 0 0 1 1 2006 0)))))
(with-test-prefix "julian-day->date"
(pass-if-equal "0002-07-29T12:00:00Z" "0002-07-29T12:00:00Z"
(date->string (julian-day->date 1722000 0) "~4"))
(pass-if-equal "0024-06-23T12:00:00Z" "0024-06-23T12:00:00Z"
(date->string (julian-day->date 1730000 0) "~4"))
(pass-if-equal "2000-01-01T00:00:00Z" "2000-01-01T00:00:00Z"
(date->string (julian-day->date 4903089/2 0) "~4")))
(with-test-prefix "time-utc->date"
(pass-if-equal "2012-07-01T00:59:59+0100" "2012-07-01T00:59:59+0100"
(date->string (time-utc->date (make-time time-utc 0 1341100799)
3600)
"~4"))
(pass-if-equal "2012-07-01T01:00:00+0100" "2012-07-01T01:00:00+0100"
(date->string (time-utc->date (make-time time-utc 0 1341100800)
3600)
"~4"))
(pass-if-equal "2012-07-01T01:00:01+0100" "2012-07-01T01:00:01+0100"
(date->string (time-utc->date (make-time time-utc 0 1341100801)
3600)
"~4")))
(with-test-prefix "time-tai->date"
(pass-if-equal "2012-07-01T00:59:59+0100" "2012-07-01T00:59:59+0100"
(date->string (time-tai->date (make-time time-tai 0 1341100833)
3600)
"~4"))
(pass-if-equal "2012-07-01T00:59:60+0100" "2012-07-01T00:59:60+0100"
(date->string (time-tai->date (make-time time-tai 0 1341100834)
3600)
"~4"))
(pass-if-equal "2012-07-01T01:00:00+0100" "2012-07-01T01:00:00+0100"
(date->string (time-tai->date (make-time time-tai 0 1341100835)
3600)
"~4"))
(pass-if-equal "2012-07-01T01:00:01+0100" "2012-07-01T01:00:01+0100"
(date->string (time-tai->date (make-time time-tai 0 1341100836)
3600)
"~4")))
(with-test-prefix "time-monotonic->date"
(pass-if-equal "2012-07-01T00:59:59+0100" "2012-07-01T00:59:59+0100"
(date->string (time-monotonic->date (make-time time-monotonic
0 1341100833)
3600)
"~4"))
(pass-if-equal "2012-07-01T00:59:60+0100" "2012-07-01T00:59:60+0100"
(date->string (time-monotonic->date (make-time time-monotonic
0 1341100834)
3600)
"~4"))
(pass-if-equal "2012-07-01T01:00:00+0100" "2012-07-01T01:00:00+0100"
(date->string (time-monotonic->date (make-time time-monotonic
0 1341100835)
3600)
"~4"))
(pass-if-equal "2012-07-01T01:00:01+0100" "2012-07-01T01:00:01+0100"
(date->string (time-monotonic->date (make-time time-monotonic
0 1341100836)
3600)
"~4")))
(with-test-prefix "time-tai->julian-day"
(pass-if-equal "2012-07-01T00:59:59+0100" 212207860799/86400
(time-tai->julian-day (make-time time-tai 0 1341100833)))
(pass-if-equal "2012-07-01T00:59:60+0100" 4912219/2
(time-tai->julian-day (make-time time-tai 0 1341100834)))
(pass-if-equal "2012-07-01T01:00:00+0100" 4912219/2
(time-tai->julian-day (make-time time-tai 0 1341100835)))
(pass-if-equal "2012-07-01T01:00:01+0100" 212207860801/86400
(time-tai->julian-day (make-time time-tai 0 1341100836))))
(with-test-prefix "time-monotonic->julian-day"
(pass-if-equal "2012-07-01T00:59:59+0100" 212207860799/86400
(time-monotonic->julian-day (make-time time-monotonic 0 1341100833)))
(pass-if-equal "2012-07-01T00:59:60+0100" 4912219/2
(time-monotonic->julian-day (make-time time-monotonic 0 1341100834)))
(pass-if-equal "2012-07-01T01:00:00+0100" 4912219/2
(time-monotonic->julian-day (make-time time-monotonic 0 1341100835)))
(pass-if-equal "2012-07-01T01:00:01+0100" 212207860801/86400
(time-monotonic->julian-day (make-time time-monotonic 0 1341100836))))
(with-test-prefix "date-week-number"
(pass-if (= 0 (date-week-number (make-date 0 0 0 0 1 1 1984 0) 0)))
(pass-if (= 0 (date-week-number (make-date 0 0 0 0 7 1 1984 0) 0)))