tzoffset.pro 14.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
;+
; NAME:
;   TZOFFSET
;
; AUTHOR:
;   Craig B. Markwardt, NASA/GSFC Code 662, Greenbelt, MD 20770
;   Craig.Markwardt@nasa.gov
;
; PURPOSE:
;   Compute timezone offset from GMT for any date
;
; CALLING SEQUENCE:
;   DT = TZOFFSET(T, [/JULIAN,] [/LOCAL,] [IS_DST=is_dst])
;   DT = TZOFFSET(/NOW)
;
; DESCRIPTION: 
;
;  The function TZOFFSET computes the time zone offset between the
;  local time zone and GMT for any date.
;
;  The time zone offset is defined here as the number of seconds of
;  time West of the Greenwich Meridian.  Equivalently, it is the
;  number of seconds that must be *added* to local time in order to
;  transform it to GMT.
;
;  Here are some examples for different time zones,
;
;       Time zone     TZOFFSET()
;         UTC               0     ;; Britain
;         GMT               0
;         GMT-5        +18000     ;; United States
;         GMT+10       -36000     ;; Australia
;
;  The user may input the date, T, as either seconds elapsed since
;  1970-01-01T00:00:00, or in Julian days (if /JULIAN is set).  The
;  input time may be either expressed in the user's local time zone
;  (if /LOCAL is set) or in UTC.
;
;
; METHODS:
;
;  Since IDL does not provide a way to compute the time zone directly,
;  TZOFFSET uses indirect methods.
;  
;  Essentially, it parses the output of SYSTIME(1) and
;  SYSTIME(1,/UTC), and computes the time difference between the local
;  system and UTC.  There is a search algorithm that finds Summer-time
;  transitions.
;
;  For speed, TZOFFSET() pre-computes time zone offsets and saves them
;  for future use as a table lookup.  On a relatively modern computer
;  in 2009, a century's worth of timezone data can be pre-computed in
;  less than one second.  If the time range of interest is smaller,
;  then the pre-computations will occur more quickly than that.  Once
;  the table has been pre-computed, interpolation of the resulting
;  table is extremely fast.
;
;  The IS_DST output parameter is estimated using a heuristic.
;  Basically, if TZOFFSET() increases, that is considered to be a
;  summer-time transition, and if TZOFFSET() decreases, that is
;  considered a transition to standard time.
;
; CAVEATS:
;
;  The results of TZOFFSET are only as good as your operating system's
;  timezone information.  If your system's timezone tables are
;  incomplete or erroneous, then so will be TZOFFSET's output.
;
;  TZOFFSET computes the timezone offsets for your system's current
;  time-zone.  To compute the offset for another different time zone,
;  you will need to reset your system's notion of the timezone.  On
;  Unix and Mac OS X systems, this can be done by setting the "TZ"
;  environment variable with SETENV.
;
;  For 32-bit Unix systems, timezone tables apparently run out in
;  2038.
;
;  Pre-computed timezone tables document Summer-time transitions to
;  within one second.  Users should avoid calling TZOFFSET() with
;  times exactly on the transition boundaries.
;
;  The IS_DST heuristic may not be perfect.  It is better to rely on
;  the actual timezone offset than to assume that IS_DST means
;  something.
;
; PARAMETERS:
;
;   T - input times, either array or scalar.  The times may be
;       in Julian days (if /JULIAN is set) or in seconds from
;       1970-01-01T00:00:00.  The times should be expressed in
;       the UTC timezone, or the local time zone if /LOCAL is set.
;
;
; RETURNS:
;
;   The resulting timezone offsets.  The return value will
;   have the same number of elements as the input T parameter.
;   See CAVEATS above.
;
; KEYWORD PARAMETERS:
;
;   IS_DST - upon return, IS_DST is set to an array containing a
;            boolean flag for each input time.  If the flag equals 1,
;            the corresponding input time is probably during "summer
;            time."  A flag value of 0 indicates probable
;            standard time.  See CAVEATS above.
;
;   JULIAN - if set, then the input times must be in Julian days.
;            DEFAULT: not set, i.e. input times are in seconds from 1970.
;
;   LOCAL - if set, then the input times must be measured in the local
;           timezone.
;           DEFAULT: not set, i.e. input times are in UTC timezone.
;
;   NOW - if set, then compute the timezone offset at the current
;         moment.  The values of T, JULIAN and LOCAL are ignored.
;
; SEE ALSO:
;
;   SYSTIME
;
; MODIFICATION HISTORY:
;   Written, CM, 14 Sep 2009
;   Documentation typos, CM, 25 Sep 2012
;   Documentation, CM, 2013-09-14
;
;  $Id: tzoffset.pro,v 1.7 2013/09/30 02:04:32 cmarkwar Exp $
;
;-
; Copyright (C) 2009, 2013, Craig Markwardt
; This software is provided as is without any warranty whatsoever.
; Permission to use, copy, modify, and distribute modified or
; unmodified copies is granted, provided this copyright and disclaimer
; are included unchanged.
;-


;; ==================================================================
;; Initialize the lookup table with known good state
pro tzoffset_init, tlimits, tgrid, toff
  COMPILE_OPT strictarr
  tutc = systime(1, /julian, /utc)  ;; [day]
  tloc = systime(1, /julian)        ;; [day]

  ;; We round because since the two SYSTIME()'s are called a
  ;; few microseconds apart, they will not correspond to exactly the
  ;; same time, although it will be close.
  toff1 = round((tutc - tloc)*86400d)+0d ;; [sec]

  ;; Table has at least two entries so that the rest of the machinery
  ;; after this works properly, but for initialization purposes it is
  ;; the same value.
  tlimits = [tutc, tutc]   ;; [day]
  tgrid   = tlimits        ;; [day]
  toff    = [toff1, toff1] ;; [sec]

  return
end

;; Convert IDL time-string to YMDhms array
function tzoffset_str2jd, str
  COMPILE_OPT strictarr
  n = n_elements(str)
  monstr = strupcase(strmid(str,4,3)) ;; Month string
  mon = intarr(n)

  ;; Parse month string (I hope it's in English!!!)
  for i = 0L, n-1 do begin
     case monstr[i] of
        'JAN': mon[i] =  1
        'FEB': mon[i] =  2
        'MAR': mon[i] =  3
        'APR': mon[i] =  4
        'MAY': mon[i] =  5
        'JUN': mon[i] =  6
        'JUL': mon[i] =  7
        'AUG': mon[i] =  8
        'SEP': mon[i] =  9
        'OCT': mon[i] = 10
        'NOV': mon[i] = 11
        'DEC': mon[i] = 12
        else: message, 'ERROR: unrecognized month "'+monstr+'"'
     endcase
  endfor

  ;; Year, day are easier to parse because they are numerical
  yr  = fix(strmid(str,20,4))
  day = fix(strmid(str,8,2))

  ;; hour minute seconds
  hr  = fix(strmid(str,11,2))
  mi  = fix(strmid(str,14,2))
  sec = fix(strmid(str,17,2))

  jd = julday(mon, day, yr, hr, mi, sec)
;  print, yr, mon, day, hr, mi, sec, jd, $
;         format='(%"%04d-%02d-%02dT%02d:%02d:%02d = JD%25.6f")'
  return, jd
end

;; Compute the time zone offset for a single requested time
;;   Time T in Julian days
function tzoffset_calc, t
  COMPILE_OPT strictarr
  jd0 = 2440587.5D ;; JD of 1970

  t1 = (t-jd0) * 86400d     ;; [sec] from 1970
  ;; Vectorize for slight speed
  ;; Requested time in UTC ......   Local
  t_str = [ systime(0,t1,/utc), systime(0,t1) ]

  ;; Calendar dates for UTC and local time
  tt   = tzoffset_str2jd(t_str)
  tutc = tt[0]
  tloc = tt[1]

  ;; Compute the (UTC - Local) time and convert from days to seconds
  tzoff = round((tutc - tloc)*86400d)+0d
;  print, 'TZOFF = ', tzoff
  return, tzoff
end

;; ==================================================================
;; Extend the existing table in +TIME direction
;;    TSTOP - Julian day of last requested time (input)
;;    TLIMITS - [START, STOP] of table (input & output)
;;    TGRID - Sample grid of Julian days for known time zone offsets
;;    TOFF - time offset value at TGRID sample points (seconds)
;;    TSTEP - time search increment (days)
;;           (should be a small fraction of a year to capture DST transitions)
pro tzoffset_extendp, tstop, tlimits, tgrid, toff, tstep=tstep
  COMPILE_OPT strictarr

  n = n_elements(tgrid)
  while tstop GE tgrid[n-1] do begin
     tnext = tgrid[n-1] + tstep
     tzoff1 = tzoffset_calc(tnext)

     if tzoff1 NE toff[n-1] then begin
        ;; We found a new transition
        tgrid = [tgrid, tnext]
        toff  = [toff,  tzoff1]
        n = n + 1

        ;; Binary search for actual transition time
        t1 = tgrid[n-2]   & t2 = tgrid[n-1]
        toff1 = toff[n-2] & toff2 = toff[n-1]
        ;; Search to 1 second accuracy ( = 1/86400th of day)
        while t2 GT t1 + 1d/86400d do begin
           tnext = (t1 + t2)/2d ;; midpoint
           tzoffx = tzoffset_calc(tnext)
           if tzoffx EQ toff1 then begin
              t1 = tnext  ;; bracketed transition from left
           endif else begin
              t2 = tnext  ;; .. from right
           endelse
        endwhile

        tgrid[n-1] = t2
     endif else begin
        ;; There was no transition, but we still record that we
        ;; stepped this far while seeing no change in time zone offset

        if toff[n-2] EQ toff[n-1] then begin
           tgrid[n-1] = tnext
        endif else begin
           tgrid = [tgrid, tnext]
           toff  = [toff, tzoff1]
           n = n + 1
        endelse
     endelse
  endwhile

  ;; Update the table limits
  tlimits[1] = tgrid[n-1]
end

;; ==================================================================
;; Extend the existing table in -TIME direction
;;    TSTOP - Julian day of earliest requested time (input)
;;    TLIMITS - [START, STOP] of table (input & output)
;;    TGRID - Sample grid of Julian days for known time zone offsets
;;    TOFF - time offset value at TGRID sample points (seconds)
;;    TSTEP - time search increment (days)
;;           (should be a small fraction of a year to capture DST transitions)
pro tzoffset_extendm, tstop, tlimits, tgrid, toff, tstep=tstep
  COMPILE_OPT strictarr

  n = n_elements(tgrid)
  while tstop LE tgrid[0] do begin
     tnext = tgrid[0] - tstep
     tzoff1 = tzoffset_calc(tnext)

     if tzoff1 NE toff[0] then begin
        ;; We found a new transition
        tgrid = [tnext, tgrid]
        toff  = [tzoff1, toff ]
        n = n + 1

        ;; Binary search for actual transition time
        t1 = tgrid[0]   & t2 = tgrid[1]
        toff1 = toff[0] & toff2 = toff[1]
        ;; Search to 1 second accuracy ( = 1/86400th of day)
        while t2 GT t1 + 1d/86400d do begin
           tnext = (t1 + t2)/2d ;; midpoint
           tzoffx = tzoffset_calc(tnext)
           if tzoffx EQ toff1 then begin
              t1 = tnext  ;; bracketed transition from left
           endif else begin
              t2 = tnext  ;; .. from right
           endelse
        endwhile

        tgrid[1] = t2
     endif else begin
        ;; There was no transition, but we still record that we
        ;; stepped this far while seeing no change in time zone offset

        if toff[0] EQ toff[1] then begin
           tgrid[0] = tnext
        endif else begin
           tgrid = [tnext, tgrid]
           toff  = [tzoff1, toff]
           n = n + 1
        endelse
     endelse

  endwhile

  ;; Update the table limits
  tlimits[0] = tgrid[0]
end

;; ==================================================================
;; Estimate IS_DST based on time zone data
pro tzoffset_dst, tgrid, toff, dst
  COMPILE_OPT strictarr
  if n_elements(toff) EQ 0 then begin
     dst = [0]
     return
  endif

  dst = intarr(n_elements(toff)) - 1
  iprev = 0L
  for i = 1, n_elements(toff)-1 do begin
     if toff[i] EQ toff[i-1] then begin
        dst[i] = dst[iprev]
     endif else if toff[i] GT toff[i-1] then begin
        dst[i] = 0
        iprev = i
     endif else if toff[i] LT toff[i-1] then begin
        dst[i] = 1
        iprev = i
     endif
  endfor
  
  wh = where(dst LT 0, ct)
  if ct EQ 0 then return
  if ct EQ n_elements(dst) then begin
     dst[*] = 0
     return
  endif
  dst[wh] = 1-dst[max(wh)+1]

  return
end

;; ==================================================================
;; Main routine
function tzoffset, tt, julian=julian, now=now, local=local, is_dst=dst, $
                   reset=reset

  COMPILE_OPT strictarr
  common tzoffset, tlimits, tgrid, toff, is_dst

  if keyword_set(reset) then begin
     if n_elements(tlimits) GT 0 then begin
        dummy = temporary(tlimits)
        dummy = temporary(tgrid)
        dummy = temporary(toff)
     endif
     return, !values.d_nan
 endif

 if n_params() EQ 0 AND NOT keyword_set(now) then begin
     message, 'USAGE: OFF = TZOFFSET(T, [/JULIAN,] [/LOCAL])', /INFO
     message, '       OFF = TZOFFSET(/NOW)',                   /INFO
     return, !values.d_nan
 endif

  tstep = 30d  ;; [day] - timezone changes occur less frequently than TSTEP

  ;; If NOW is set, then retrieve the current Julian date in UTC time zone.
  if keyword_set(now) then begin
     tt = systime(1, /julian)
     julian = 1 & local = 0
  endif

  if keyword_set(julian) then begin
     t1 = tt
  endif else begin
     t1 = 2440587.5D + tt/86400d
  endelse

  mint = min(tt, max=maxt)

  ;; --------------
  ;; Initialize look-up table data the first time
  if n_elements(tlimits) EQ 0 then begin
     ;; Initialize the look-up table...
     tzoffset_init, tlimits, tgrid, toff
     ;; ... with at least a year on either side
     tzoffset_extendp, tlimits[1]+365d, tlimits, tgrid, toff, tstep=tstep
     tzoffset_extendm, tlimits[0]-365d, tlimits, tgrid, toff, tstep=tstep
     ;; ... and estimate summer time flag
     tzoffset_dst, tgrid, toff, is_dst
  endif


  ;; --------------
  ;; Extend the range of the look-up table if the input range
  ;; exceeds the table range
  extended = 0
  if mint LE tlimits[0] then begin
     tzoffset_extendm, mint, tlimits, tgrid, toff, tstep=tstep
     extended = 1
  endif
  if maxt GE tlimits[1] then begin
     tzoffset_extendp, maxt, tlimits, tgrid, toff, tstep=tstep
     extended = 1
  endif
  ;; Get DST data for new time grid
  if extended OR n_elements(is_dst) then begin
     tzoffset_dst, tgrid, toff, is_dst
  endif

  ;; --------------
  ;; Here is where we handle the user's specifically requested times
  ;; Find the positions of the requested times T1 in the look-up table
  ;; sample grid.
  ii = value_locate(tgrid, t1)

  ;; Retrieve the values by lookup
  tzoff = toff[ii]
  dst   = is_dst[ii]
  
  ;; Input time is a local time, therefore, do one iteration by adding
  ;; time zone offset and recomputing offset.  This will only change
  ;; things if the requested times are near a DST transition point.
  if keyword_set(local) then $
     return, tzoffset(t1+tzoff/86400d, /julian, is_dst=dst)

  return, tzoff
end