]> code.delx.au - gnu-emacs/blobdiff - lisp/calendar/cal-dst.el
Do not assume DST starts/ends on the same date in every year.
[gnu-emacs] / lisp / calendar / cal-dst.el
index d395173197e7eb867805675aa450921ee0d25169..9604a4debbcadd6e455956e8b80cfeeb7f29dd94 100644 (file)
@@ -1,28 +1,30 @@
-;;; cal-dst.el --- calendar functions for daylight savings rules.
+;;; cal-dst.el --- calendar functions for daylight savings rules
 
-;; Copyright (C) 1993 Free Software Foundation, Inc.
+;; Copyright (C) 1993, 1994, 1995, 1996, 2001, 2002, 2003, 2004, 2005,
+;;   2006  Free Software Foundation, Inc.
 
 ;; Author: Paul Eggert <eggert@twinsun.com>
 ;;     Edward M. Reingold <reingold@cs.uiuc.edu>
+;; Maintainer: Glenn Morris <rgm@gnu.org>
 ;; Keywords: calendar
 ;; Human-Keywords: daylight savings time, calendar, diary, holidays
 
 ;; This file is part of GNU Emacs.
 
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 2, or (at your option)
+;; any later version.
+
 ;; GNU Emacs is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY.  No author or distributor
-;; accepts responsibility to anyone for the consequences of using it
-;; or for whether it serves any particular purpose or works at all,
-;; unless he says so in writing.  Refer to the GNU Emacs General Public
-;; License for full details.
-
-;; Everyone is granted permission to copy, modify and redistribute
-;; GNU Emacs, but only under the conditions described in the
-;; GNU Emacs General Public License.   A copy of this license is
-;; supposed to have been given to you along with GNU Emacs so you
-;; can know your rights and responsibilities.  It should be in a
-;; file named COPYING.  Among other things, the copyright notice
-;; and this notice must be preserved on all copies.
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING.  If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
 
 ;;; Commentary:
 
 ;;; Code:
 
 (require 'calendar)
+(require 'cal-persia)
+
+(defcustom calendar-dst-check-each-year-flag t
+  "Non-nil means to check each year for DST transitions as needed.
+nil means to assume the next two transitions found after the
+current date apply to all years.  This is faster, but not always
+correct, since the dates of Daylight Saving transitions sometimes
+change."
+  :type 'boolean
+  :version "22.1"
+  :group 'calendar)
 
 (defvar calendar-current-time-zone-cache nil
   "Cache for result of calendar-current-time-zone.")
@@ -69,14 +82,14 @@ absolute date ABS-DATE is the equivalent moment to X."
 (defun calendar-time-from-absolute (abs-date s)
   "Time of absolute date ABS-DATE, S seconds after midnight.
 
-Returns the pair (HIGH . LOW) where HIGH and LOW are the high and low
+Returns the list (HIGH LOW) where HIGH and LOW are the high and low
 16 bits, respectively, of the number of seconds 1970-01-01 00:00:00 UTC,
 ignoring leap seconds, that is the equivalent moment to S seconds after
 midnight UTC on absolute date ABS-DATE."
   (let* ((a (- abs-date calendar-system-time-basis))
          (u (+ (* 163 (mod a 512)) (floor s 128))))
     ;; Overflow is a terrible thing!
-    (cons
+    (list
      ;; floor((60*60*24*a + s) / 2^16)
      (+ a (* 163 (floor a 512)) (floor u 512))
      ;; (60*60*24*a + s) mod 2^16
@@ -128,7 +141,7 @@ Return nil if no such transition can be found."
 
 (defun calendar-time-zone-daylight-rules (abs-date utc-diff)
   "Return daylight transition rule for ABS-DATE, UTC-DIFF sec offset from UTC.
-ABS-DIFF must specify a day that contains a daylight savings transition.
+ABS-DATE must specify a day that contains a daylight savings transition.
 The result has the proper form for calendar-daylight-savings-starts'."
   (let* ((date (calendar-gregorian-from-absolute abs-date))
         (weekday (% abs-date 7))
@@ -153,7 +166,16 @@ The result has the proper form for calendar-daylight-savings-starts'."
                      (cons
                       (list 'calendar-nth-named-day 1 weekday m 'year j)
                       l)))
-            l)))
+            l)
+          ;; 01-01 and 07-01 for this year's Persian calendar.
+          (if (and (= m 3) (<= 20 d) (<= d 21))
+              '((calendar-gregorian-from-absolute
+                 (calendar-absolute-from-persian
+                  (list 1 1 (- year 621))))))
+          (if (and (= m 9) (<= 22 d) (<= d 23))
+              '((calendar-gregorian-from-absolute
+                 (calendar-absolute-from-persian
+                  (list 7 1 (- year 621))))))))
         (prevday-sec (- -1 utc-diff)) ;; last sec of previous local day
         (year (1+ y)))
     ;; Scan through the next few years until only one rule remains.
@@ -187,6 +209,74 @@ The result has the proper form for calendar-daylight-savings-starts'."
          (cdr candidate-rules)))
     (car candidate-rules)))
 
+;; TODO it might be better to extract this information directly from
+;; the system timezone database. But cross-platform...?
+;; See thread
+;; http://lists.gnu.org/archive/html/emacs-pretest-bug/2006-11/msg00060.html
+(defun calendar-dst-find-data (&optional time)
+  "Find data on the first Daylight Saving Time transitions after TIME.
+TIME defaults to `current-time'.  Return value is as described
+for `calendar-current-time-zone'."
+  (let* ((t0 (or time (current-time)))
+         (t0-zone (current-time-zone t0))
+         (t0-utc-diff (car t0-zone))
+         (t0-name (car (cdr t0-zone))))
+    (if (not t0-utc-diff)
+        ;; Little or no time zone information is available.
+        (list nil nil t0-name t0-name nil nil nil nil)
+      (let* ((t1 (calendar-next-time-zone-transition t0))
+             (t2 (and t1 (calendar-next-time-zone-transition t1))))
+        (if (not t2)
+            ;; This locale does not have daylight savings time.
+            (list (/ t0-utc-diff 60) 0 t0-name t0-name nil nil 0 0)
+          ;; Use heuristics to find daylight savings parameters.
+          (let* ((t1-zone (current-time-zone t1))
+                 (t1-utc-diff (car t1-zone))
+                 (t1-name (car (cdr t1-zone)))
+                 (t1-date-sec (calendar-absolute-from-time t1 t0-utc-diff))
+                 (t2-date-sec (calendar-absolute-from-time t2 t1-utc-diff))
+                 ;; TODO When calendar-dst-check-each-year-flag is non-nil,
+                 ;; the rules can be simpler than they currently are.
+                 (t1-rules (calendar-time-zone-daylight-rules
+                            (car t1-date-sec) t0-utc-diff))
+                 (t2-rules (calendar-time-zone-daylight-rules
+                            (car t2-date-sec) t1-utc-diff))
+                 (t1-time (/ (cdr t1-date-sec) 60))
+                 (t2-time (/ (cdr t2-date-sec) 60)))
+            (cons
+             (/ (min t0-utc-diff t1-utc-diff) 60)
+             (cons
+              (/ (abs (- t0-utc-diff t1-utc-diff)) 60)
+              (if (< t0-utc-diff t1-utc-diff)
+                  (list t0-name t1-name t1-rules t2-rules t1-time t2-time)
+                (list t1-name t0-name t2-rules t1-rules t2-time t1-time)
+                )))))))))
+
+(defvar calendar-dst-transition-cache nil
+  "Internal cal-dst variable storing date of Daylight Saving Time transitions.
+Value is a list with elements of the form (YEAR START END), where
+START and END are expressions that when evaluated return the
+start and end dates (respectively) for DST in YEAR. Used by the
+function `calendar-dst-find-startend'.")
+
+(defun calendar-dst-find-startend (year)
+  "Find the dates in YEAR on which Daylight Saving Time starts and ends.
+Returns a list (YEAR START END), where START and END are
+expressions that when evaluated return the start and end dates,
+respectively. This function first attempts to use pre-calculated
+data from `calendar-dst-transition-cache', otherwise it calls
+`calendar-dst-find-data' (and adds the results to the cache)."
+  (let ((e (assoc year calendar-dst-transition-cache))
+        f)
+    (or e
+        (progn
+          (setq e (calendar-dst-find-data (encode-time 1 0 0 1 1 year))
+                f (nth 4 e)
+                e (list year f (nth 5 e))
+                calendar-dst-transition-cache
+                (append calendar-dst-transition-cache (list e)))
+          e))))
+
 (defun calendar-current-time-zone ()
   "Return UTC difference, dst offset, names and rules for current time zone.
 
@@ -214,44 +304,10 @@ DST-ZONE are equal, and all the DST-* integer variables are 0.
 Some operating systems cannot provide all this information to Emacs; in this
 case, `calendar-current-time-zone' returns a list containing nil for the data
 it can't find."
-  (or
-   calendar-current-time-zone-cache
-   (setq
-    calendar-current-time-zone-cache
-    (let* ((t0 (current-time))
-          (t0-zone (current-time-zone t0))
-          (t0-utc-diff (car t0-zone))
-          (t0-name (car (cdr t0-zone))))
-      (if (not t0-utc-diff)
-         ;; Little or no time zone information is available.
-         (list nil nil t0-name t0-name nil nil nil nil)
-       (let* ((t1 (calendar-next-time-zone-transition t0))
-              (t2 (and t1 (calendar-next-time-zone-transition t1))))
-         (if (not t2)
-             ;; This locale does not have daylight savings time.
-             (list (/ t0-utc-diff 60) 0 t0-name t0-name nil nil 0 0)
-           ;; Use heuristics to find daylight savings parameters.
-           (let* ((t1-zone (current-time-zone t1))
-                  (t1-utc-diff (car t1-zone))
-                  (t1-name (car (cdr t1-zone)))
-                  (t1-date-sec (calendar-absolute-from-time t1 t0-utc-diff))
-                  (t2-date-sec (calendar-absolute-from-time t2 t1-utc-diff))
-                  (t1-rules (calendar-time-zone-daylight-rules
-                             (car t1-date-sec) t0-utc-diff))
-                  (t2-rules (calendar-time-zone-daylight-rules
-                             (car t2-date-sec) t1-utc-diff))
-                  (t1-time (/ (cdr t1-date-sec) 60))
-                  (t2-time (/ (cdr t2-date-sec) 60)))
-             (cons
-              (/ (min t0-utc-diff t1-utc-diff) 60)
-              (cons
-               (/ (abs (- t0-utc-diff t1-utc-diff)) 60)
-               (if (< t0-utc-diff t1-utc-diff)
-                   (list t0-name t1-name t1-rules t2-rules t2-time t1-time)
-                   (list t1-name t0-name t2-rules t1-rules t1-time t2-time)
-                   )))))))))))
-
-;;; The following six defvars relating to daylight savings time should NOT be
+  (unless calendar-current-time-zone-cache
+    (setq calendar-current-time-zone-cache (calendar-dst-find-data))))
+
+;;; The following eight defvars relating to daylight savings time should NOT be
 ;;; marked to go into loaddefs.el where they would be evaluated when Emacs is
 ;;; dumped.  These variables' appropriate values depend on the conditions under
 ;;; which the code is INVOKED; so it's inappropriate to initialize them when
@@ -268,7 +324,7 @@ example, -300 for New York City, -480 for Los Angeles.")
 (defvar calendar-daylight-time-offset
   (or (car (cdr calendar-current-time-zone-cache)) 60)
   "*Number of minutes difference between daylight savings and standard time.
-  
+
 If the locale never uses daylight savings time, set this to 0.")
 
 (defvar calendar-standard-time-zone-name
@@ -280,13 +336,33 @@ For example, \"EST\" in New York City, \"PST\" for Los Angeles.")
   (or (car (nthcdr 3 calendar-current-time-zone-cache)) "EDT")
   "*Abbreviated name of daylight-savings time zone at `calendar-location-name'.
 For example, \"EDT\" in New York City, \"PDT\" for Los Angeles.")
-  
+
+
+(defun calendar-dst-starts (year)
+  "Return the date of YEAR on which Daylight Saving Time starts.
+This function respects the value of `calendar-dst-check-each-year-flag'."
+  (or (let ((expr (if calendar-dst-check-each-year-flag
+                      (cadr (calendar-dst-find-startend year))
+                    (nth 4 calendar-current-time-zone-cache))))
+        (if expr (eval expr)))
+      (and (not (zerop calendar-daylight-time-offset))
+           (calendar-nth-named-day 1 0 4 year))))
+
+(defun calendar-dst-ends (year)
+  "Return the date of YEAR on which Daylight Saving Time ends.
+This function respects the value of `calendar-dst-check-each-year-flag'."
+  (or (let ((expr (if calendar-dst-check-each-year-flag
+                      (nth 2 (calendar-dst-find-startend year))
+                    (nth 5 calendar-current-time-zone-cache))))
+        (if expr (eval expr)))
+      (and (not (zerop calendar-daylight-time-offset))
+           (calendar-nth-named-day -1 0 10 year))))
+
+
 ;;;###autoload
 (put 'calendar-daylight-savings-starts 'risky-local-variable t)
 (defvar calendar-daylight-savings-starts
-  (or (car (nthcdr 4 calendar-current-time-zone-cache))
-      (and (not (zerop calendar-daylight-time-offset))
-          '(calendar-nth-named-day 1 0 4 year)))
+  '(calendar-dst-starts year)
   "*Sexp giving the date on which daylight savings time starts.
 This is an expression in the variable `year' whose value gives the Gregorian
 date in the form (month day year) on which daylight savings time starts.  It is
@@ -305,11 +381,9 @@ If it starts on the first Sunday in April, you would set it to
 If the locale never uses daylight savings time, set this to nil.")
 
 ;;;###autoload
-(put 'calendar-daylight-savings-starts 'risky-local-variable t)
+(put 'calendar-daylight-savings-ends 'risky-local-variable t)
 (defvar calendar-daylight-savings-ends
-  (or (car (nthcdr 5 calendar-current-time-zone-cache))
-      (and (not (zerop calendar-daylight-time-offset))
-          '(calendar-nth-named-day -1 0 10 year)))
+  '(calendar-dst-ends year)
   "*Sexp giving the date on which daylight savings time ends.
 This is an expression in the variable `year' whose value gives the Gregorian
 date in the form (month day year) on which daylight savings time ends.  It is
@@ -321,16 +395,68 @@ For example, if daylight savings time ends on the last Sunday in October:
       '(calendar-nth-named-day -1 0 10 year)
 
 If the locale never uses daylight savings time, set this to nil.")
-  
+
 (defvar calendar-daylight-savings-starts-time
   (or (car (nthcdr 6 calendar-current-time-zone-cache)) 120)
   "*Number of minutes after midnight that daylight savings time starts.")
-  
+
 (defvar calendar-daylight-savings-ends-time
   (or (car (nthcdr 7 calendar-current-time-zone-cache))
       calendar-daylight-savings-starts-time)
   "*Number of minutes after midnight that daylight savings time ends.")
 
+(defun dst-in-effect (date)
+  "True if on absolute DATE daylight savings time is in effect.
+Fractional part of DATE is local standard time of day."
+  (let* ((year (extract-calendar-year
+                (calendar-gregorian-from-absolute (floor date))))
+         (dst-starts-gregorian (eval calendar-daylight-savings-starts))
+         (dst-ends-gregorian (eval calendar-daylight-savings-ends))
+         (dst-starts (and dst-starts-gregorian
+                          (+ (calendar-absolute-from-gregorian
+                              dst-starts-gregorian)
+                             (/ calendar-daylight-savings-starts-time
+                                60.0 24.0))))
+         (dst-ends (and dst-ends-gregorian
+                        (+ (calendar-absolute-from-gregorian
+                            dst-ends-gregorian)
+                           (/ (- calendar-daylight-savings-ends-time
+                                 calendar-daylight-time-offset)
+                              60.0 24.0)))))
+    (and dst-starts dst-ends
+         (if (< dst-starts dst-ends)
+             (and (<= dst-starts date) (< date dst-ends))
+           (or (<= dst-starts date) (< date dst-ends))))))
+
+(defun dst-adjust-time (date time &optional style)
+  "Adjust, to account for dst on DATE, decimal fraction standard TIME.
+Returns a list (date adj-time zone) where `date' and `adj-time' are the values
+adjusted for `zone'; here `date' is a list (month day year), `adj-time' is a
+decimal fraction time, and `zone' is a string.
+
+Optional parameter STYLE forces the result time to be standard time when its
+value is 'standard and daylight savings time (if available) when its value is
+'daylight.
+
+Conversion to daylight savings time is done according to
+`calendar-daylight-savings-starts', `calendar-daylight-savings-ends',
+`calendar-daylight-savings-starts-time',
+`calendar-daylight-savings-ends-time', and
+`calendar-daylight-savings-offset'."
+
+  (let* ((rounded-abs-date (+ (calendar-absolute-from-gregorian date)
+                             (/ (round (* 60 time)) 60.0 24.0)))
+         (dst (dst-in-effect rounded-abs-date))
+        (time-zone (if dst
+                       calendar-daylight-time-zone-name
+                       calendar-standard-time-zone-name))
+        (time (+ rounded-abs-date
+                  (if dst (/ calendar-daylight-time-offset 24.0 60.0) 0))))
+    (list (calendar-gregorian-from-absolute (truncate time))
+          (* 24.0 (- time (truncate time)))
+          time-zone)))
+
 (provide 'cal-dst)
 
+;;; arch-tag: a141d204-213c-4ca5-bdc6-f9df3aa92aad
 ;;; cal-dst.el ends here