Blame view

src/idl_extern/CMTotal_for_Dustemwrap/tzoffset.pro 14.2 KB
517b8f98   Annie Hughes   first commit
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