forked from Unidata/netcdf4-python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
netcdftime.py
1174 lines (961 loc) · 40.3 KB
/
netcdftime.py
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
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Performs conversions of netCDF time coordinate data to/from datetime objects.
"""
import math, numpy, re, time
from datetime import datetime as real_datetime
from datetime import tzinfo, timedelta
from calendar import monthrange
_units = ['days','hours','minutes','seconds','day','hour','minute','second']
_calendars = ['standard','gregorian','proleptic_gregorian','noleap','julian','all_leap','365_day','366_day','360_day']
__version__ = '1.0'
# Adapted from http://delete.me.uk/2005/03/iso8601.html
ISO8601_REGEX = re.compile(r"(?P<year>[0-9]{1,4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
r"(((?P<separator1>.)(?P<hour>[0-9]{1,2}):(?P<minute>[0-9]{1,2})(:(?P<second>[0-9]{1,2})(\.(?P<fraction>[0-9]+))?)?)?"
r"((?P<separator2>.?)(?P<timezone>Z|(([-+])([0-9]{1,2}):([0-9]{1,2}))))?)?)?)?"
)
TIMEZONE_REGEX = re.compile("(?P<prefix>[+-])(?P<hours>[0-9]{1,2}):(?P<minutes>[0-9]{1,2})")
class datetime:
"""
Phony datetime object which mimics the python datetime object,
but allows for dates that don't exist in the proleptic gregorian calendar.
Doesn't do timedelta operations, doesn't overload + and -.
Has strftime, timetuple and __repr__ methods. The format
of the string produced by __repr__ is controlled by self.format
(default %Y-%m-%d %H:%M:%S).
Instance variables are year,month,day,hour,minute,second,dayofwk,dayofyr
and format.
"""
def __init__(self,year,month,day,hour=0,minute=0,second=0,dayofwk=-1,dayofyr=1):
"""dayofyr set to 1 by default - otherwise time.strftime will complain"""
self.year=year
self.month=month
self.day=day
self.hour=hour
self.minute=minute
self.dayofwk=dayofwk
self.dayofyr=dayofyr
self.second=second
self.format='%Y-%m-%d %H:%M:%S'
def strftime(self,format=None):
if format is None:
format = self.format
return _strftime(self,format)
def timetuple(self):
return (self.year,self.month,self.day,self.hour,self.minute,self.second,self.dayofwk,self.dayofyr,-1)
def __repr__(self):
return self.strftime(self.format)
def __eq__(self, date):
return self.strftime('%Y-%m-%d %H:%M:%S') == date.strftime('%Y-%m-%d %H:%M:%S')
def JulianDayFromDate(date,calendar='standard'):
"""
creates a Julian Day from a 'datetime-like' object. Returns the fractional
Julian Day (resolution 1 second).
if calendar='standard' or 'gregorian' (default), Julian day follows Julian
Calendar on and before 1582-10-5, Gregorian calendar after 1582-10-15.
if calendar='proleptic_gregorian', Julian Day follows gregorian calendar.
if calendar='julian', Julian Day follows julian calendar.
Algorithm:
Meeus, Jean (1998) Astronomical Algorithms (2nd Edition). Willmann-Bell,
Virginia. p. 63
"""
# based on redate.py by David Finlayson.
year=date.year; month=date.month; day=date.day
hour=date.hour; minute=date.minute; second=date.second
# Convert time to fractions of a day
day = day + hour/24.0 + minute/1440.0 + second/86400.0
# Start Meeus algorithm (variables are in his notation)
if (month < 3):
month = month + 12
year = year - 1
A = int(year/100)
# MC
# jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + \
# day - 1524.5
jd = 365.*year + int(0.25 * year + 2000.) + int(30.6001 * (month + 1)) + \
day + 1718994.5
# optionally adjust the jd for the switch from
# the Julian to Gregorian Calendar
# here assumed to have occurred the day after 1582 October 4
if calendar in ['standard','gregorian']:
if jd >= 2299170.5:
# 1582 October 15 (Gregorian Calendar)
B = 2 - A + int(A/4)
elif jd < 2299160.5:
# 1582 October 5 (Julian Calendar)
B = 0
else:
raise ValueError('impossible date (falls in gap between end of Julian calendar and beginning of Gregorian calendar')
elif calendar == 'proleptic_gregorian':
B = 2 - A + int(A/4)
elif calendar == 'julian':
B = 0
else:
raise ValueError('unknown calendar, must be one of julian,standard,gregorian,proleptic_gregorian, got %s' % calendar)
# adjust for Julian calendar if necessary
jd = jd + B
return jd
def _NoLeapDayFromDate(date):
"""
creates a Julian Day for a calendar with no leap years from a datetime
instance. Returns the fractional Julian Day (resolution 1 second).
"""
year=date.year; month=date.month; day=date.day
hour=date.hour; minute=date.minute; second=date.second
# Convert time to fractions of a day
day = day + hour/24.0 + minute/1440.0 + second/86400.0
# Start Meeus algorithm (variables are in his notation)
if (month < 3):
month = month + 12
year = year - 1
jd = int(365. * (year + 4716)) + int(30.6001 * (month + 1)) + \
day - 1524.5
return jd
def _AllLeapFromDate(date):
"""
creates a Julian Day for a calendar where all years have 366 days from
a 'datetime-like' object.
Returns the fractional Julian Day (resolution 1 second).
"""
year=date.year; month=date.month; day=date.day
hour=date.hour; minute=date.minute; second=date.second
# Convert time to fractions of a day
day = day + hour/24.0 + minute/1440.0 + second/86400.0
# Start Meeus algorithm (variables are in his notation)
if (month < 3):
month = month + 12
year = year - 1
jd = int(366. * (year + 4716)) + int(30.6001 * (month + 1)) + \
day - 1524.5
return jd
def _360DayFromDate(date):
"""
creates a Julian Day for a calendar where all months have 30 daysfrom
a 'datetime-like' object.
Returns the fractional Julian Day (resolution 1 second).
"""
year=date.year; month=date.month; day=date.day
hour=date.hour; minute=date.minute; second=date.second
# Convert time to fractions of a day
day = day + hour/24.0 + minute/1440.0 + second/86400.0
jd = int(360. * (year + 4716)) + int(30. * (month - 1)) + day
return jd
def DateFromJulianDay(JD,calendar='standard'):
"""
returns a 'datetime-like' object given Julian Day. Julian Day is a
fractional day with a resolution of 1 second.
if calendar='standard' or 'gregorian' (default), Julian day follows Julian
Calendar on and before 1582-10-5, Gregorian calendar after 1582-10-15.
if calendar='proleptic_gregorian', Julian Day follows gregorian calendar.
if calendar='julian', Julian Day follows julian calendar.
The datetime object is a 'real' datetime object if the date falls in
the Gregorian calendar (i.e. calendar='proleptic_gregorian', or
calendar = 'standard'/'gregorian' and the date is after 1582-10-15).
Otherwise, it's a 'phony' datetime object which is actually an instance
of netcdftime.datetime.
Algorithm:
Meeus, Jean (1998) Astronomical Algorithms (2nd Edition). Willmann-Bell,
Virginia. p. 63
"""
# based on redate.py by David Finlayson.
if JD < 0:
raise ValueError('Julian Day must be positive')
dayofwk = int(math.fmod(int(JD + 1.5),7))
(F, Z) = math.modf(JD + 0.5)
Z = int(Z)
if calendar in ['standard','gregorian']:
if JD < 2299160.5:
A = Z
else:
# MC
# alpha = int((Z - 1867216.25)/36524.25)
# A = Z + 1 + alpha - int(alpha/4)
alpha = int(((Z - 1867216.)-0.25)/36524.25)
A = Z + 1 + alpha - int(0.25*alpha)
elif calendar == 'proleptic_gregorian':
# MC
# alpha = int((Z - 1867216.25)/36524.25)
# A = Z + 1 + alpha - int(alpha/4)
alpha = int(((Z - 1867216.)-0.25)/36524.25)
A = Z + 1 + alpha - int(0.25*alpha)
elif calendar == 'julian':
A = Z
else:
raise ValueError('unknown calendar, must be one of julian,standard,gregorian,proleptic_gregorian, got %s' % calendar)
B = A + 1524
# MC
# C = int((B - 122.1)/365.25)
# D = int(365.25 * C)
C = int(6680.+((B-2439870.)-122.1)/365.25)
D = 365*C + int(0.25 * C)
E = int((B - D)/30.6001)
# Convert to date
day = B - D - int(30.6001 * E) + F
nday = B-D-123
if nday <= 305:
dayofyr = nday+60
else:
dayofyr = nday-305
# MC
# if E < 14:
# month = E - 1
# else:
# month = E - 13
# if month > 2:
# year = C - 4716
# else:
# year = C - 4715
month = E - 1
if month > 12: month -= 12
year = C - 4715
if month > 2: year -= 1
if year <= 0: year -= 1
# a leap year?
leap = 0
if year % 4 == 0:
leap = 1
if calendar == 'proleptic_gregorian' or \
(calendar in ['standard','gregorian'] and JD >= 2299160.5):
if year % 100 == 0 and year % 400 != 0:
leap = 0
if leap and month > 2:
dayofyr = dayofyr + leap
# Convert fractions of a day to time
(dfrac, days) = math.modf(day/1.0)
(hfrac, hours) = math.modf(dfrac * 24.0)
(mfrac, minutes) = math.modf(hfrac * 60.0)
seconds = round(mfrac * 60.0) # seconds are rounded
if seconds > 59:
seconds = 0
minutes = minutes + 1
if minutes > 59:
minutes = 0
hours = hours + 1
if hours > 23:
hours = 0
days = days + 1
# if days exceeds number allowed in a month, flip to next month.
# this fixes issue 75.
daysinmonth = monthrange(year, month)[1]
if days > daysinmonth:
days = 1
month = month + 1
if month > 12:
month = 1
year = year + 1
# return a 'real' datetime instance if calendar is gregorian.
if calendar == 'proleptic_gregorian' or \
(calendar in ['standard','gregorian'] and JD >= 2299160.5):
return real_datetime(year,month,int(days),int(hours),int(minutes),int(seconds))
else:
# or else, return a 'datetime-like' instance.
return datetime(year,month,int(days),int(hours),int(minutes),int(seconds),dayofwk,dayofyr)
def _DateFromNoLeapDay(JD):
"""
returns a 'datetime-like' object given Julian Day for a calendar with no leap
days. Julian Day is a fractional day with a resolution of 1 second.
"""
# based on redate.py by David Finlayson.
if JD < 0:
raise ValueError('Julian Day must be positive')
dayofwk = int(math.fmod(int(JD + 1.5),7))
(F, Z) = math.modf(JD + 0.5)
Z = int(Z)
A = Z
B = A + 1524
C = int((B - 122.1)/365.)
D = int(365. * C)
E = int((B - D)/30.6001)
# Convert to date
day = B - D - int(30.6001 * E) + F
nday = B-D-123
if nday <= 305:
dayofyr = nday+60
else:
dayofyr = nday-305
if E < 14:
month = E - 1
else:
month = E - 13
if month > 2:
year = C - 4716
else:
year = C - 4715
# Convert fractions of a day to time
(dfrac, days) = math.modf(day/1.0)
(hfrac, hours) = math.modf(dfrac * 24.0)
(mfrac, minutes) = math.modf(hfrac * 60.0)
seconds = round(mfrac * 60.0) # seconds are rounded
if seconds > 59:
seconds = 0
minutes = minutes + 1
if minutes > 59:
minutes = 0
hours = hours + 1
if hours > 23:
hours = 0
days = days + 1
return datetime(year,month,int(days),int(hours),int(minutes),int(seconds), dayofwk, dayofyr)
def _DateFromAllLeap(JD):
"""
returns a 'datetime-like' object given Julian Day for a calendar where all
years have 366 days.
Julian Day is a fractional day with a resolution of 1 second.
"""
# based on redate.py by David Finlayson.
if JD < 0:
raise ValueError('Julian Day must be positive')
dayofwk = int(math.fmod(int(JD + 1.5),7))
(F, Z) = math.modf(JD + 0.5)
Z = int(Z)
A = Z
B = A + 1524
C = int((B - 122.1)/366.)
D = int(366. * C)
E = int((B - D)/30.6001)
# Convert to date
day = B - D - int(30.6001 * E) + F
nday = B-D-123
if nday <= 305:
dayofyr = nday+60
else:
dayofyr = nday-305
if E < 14:
month = E - 1
else:
month = E - 13
if month > 2:
dayofyr = dayofyr+1
if month > 2:
year = C - 4716
else:
year = C - 4715
# Convert fractions of a day to time
(dfrac, days) = math.modf(day/1.0)
(hfrac, hours) = math.modf(dfrac * 24.0)
(mfrac, minutes) = math.modf(hfrac * 60.0)
seconds = round(mfrac * 60.0) # seconds are rounded
if seconds > 59:
seconds = 0
minutes = minutes + 1
if minutes > 59:
minutes = 0
hours = hours + 1
if hours > 23:
hours = 0
days = days + 1
return datetime(year,month,int(days),int(hours),int(minutes),int(seconds), dayofwk, dayofyr)
def _DateFrom360Day(JD):
"""
returns a 'datetime-like' object given Julian Day for a calendar where all
months have 30 days.
Julian Day is a fractional day with a resolution of 1 second.
"""
if JD < 0:
raise ValueError('Julian Day must be positive')
#jd = int(360. * (year + 4716)) + int(30. * (month - 1)) + day
(F, Z) = math.modf(JD)
year = int((Z-0.5)/360.) - 4716
dayofyr = Z - (year+4716)*360
month = int((dayofyr-0.5)/30)+1
day = dayofyr - (month-1)*30 + F
# Convert fractions of a day to time
(dfrac, days) = math.modf(day/1.0)
(hfrac, hours) = math.modf(dfrac * 24.0)
(mfrac, minutes) = math.modf(hfrac * 60.0)
seconds = round(mfrac * 60.0) # seconds are rounded
if seconds > 59:
seconds = 0
minutes = minutes + 1
if minutes > 59:
minutes = 0
hours = hours + 1
if hours > 23:
hours = 0
days = days + 1
return datetime(year,month,int(days),int(hours),int(minutes),int(seconds),-1, int(dayofyr))
def _dateparse(timestr):
"""parse a string of the form time-units since yyyy-mm-dd hh:mm:ss
return a tuple (units,utc_offset, datetimeinstance)"""
timestr_split = timestr.split()
units = timestr_split[0].lower()
if units not in _units:
raise ValueError("units must be one of 'seconds', 'minutes', 'hours' or 'days' (or singular version of these), got '%s'" % units)
if timestr_split[1].lower() != 'since':
raise ValueError("no 'since' in unit_string")
# parse the date string.
n = timestr.find('since')+6
year,month,day,hour,minute,second,utc_offset = _parse_date(timestr[n:])
return units, utc_offset, datetime(year, month, day, hour, minute, second)
class utime:
"""
Performs conversions of netCDF time coordinate
data to/from datetime objects.
To initialize: C{t = utime(unit_string,calendar='standard')}
where
B{C{unit_string}} is a string of the form
C{'time-units since <time-origin>'} defining the time units.
Valid time-units are days, hours, minutes and seconds (the singular forms
are also accepted). An example unit_string would be C{'hours
since 0001-01-01 00:00:00'}.
The B{C{calendar}} keyword describes the calendar used in the time calculations.
All the values currently defined in the U{CF metadata convention
<http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.1/cf-conventions.html#time-coordinate>}
are accepted. The default is C{'standard'}, which corresponds to the mixed
Gregorian/Julian calendar used by the C{udunits library}. Valid calendars
are:
C{'gregorian'} or C{'standard'} (default):
Mixed Gregorian/Julian calendar as defined by udunits.
C{'proleptic_gregorian'}:
A Gregorian calendar extended to dates before 1582-10-15. That is, a year
is a leap year if either (i) it is divisible by 4 but not by 100 or (ii)
it is divisible by 400.
C{'noleap'} or C{'365_day'}:
Gregorian calendar without leap years, i.e., all years are 365 days long.
all_leap or 366_day Gregorian calendar with every year being a leap year,
i.e., all years are 366 days long.
C{'360_day'}:
All years are 360 days divided into 30 day months.
C{'julian'}:
Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a
leap year if it is divisible by 4.
The C{L{num2date}} and C{L{date2num}} class methods can used to convert datetime
instances to/from the specified time units using the specified calendar.
The datetime instances returned by C{num2date} are 'real' python datetime
objects if the date falls in the Gregorian calendar (i.e.
C{calendar='proleptic_gregorian', 'standard'} or C{'gregorian'} and
the date is after 1582-10-15). Otherwise, they are 'phony' datetime
objects which are actually instances of C{L{netcdftime.datetime}}. This is
because the python datetime module cannot handle the weird dates in some
calendars (such as C{'360_day'} and C{'all_leap'}) which don't exist in any real
world calendar.
Example usage:
>>> from netcdftime import utime
>>> from datetime import datetime
>>> cdftime = utime('hours since 0001-01-01 00:00:00')
>>> date = datetime.now()
>>> print date
2006-03-17 16:04:02.561678
>>>
>>> t = cdftime.date2num(date)
>>> print t
17577328.0672
>>>
>>> date = cdftime.num2date(t)
>>> print date
2006-03-17 16:04:02
>>>
The resolution of the transformation operation is 1 second.
Warning: Dates between 1582-10-5 and 1582-10-15 do not exist in the
C{'standard'} or C{'gregorian'} calendars. An exception will be raised if you pass
a 'datetime-like' object in that range to the C{L{date2num}} class method.
Words of Wisdom from the British MetOffice concerning reference dates:
"udunits implements the mixed Gregorian/Julian calendar system, as
followed in England, in which dates prior to 1582-10-15 are assumed to use
the Julian calendar. Other software cannot be relied upon to handle the
change of calendar in the same way, so for robustness it is recommended
that the reference date be later than 1582. If earlier dates must be used,
it should be noted that udunits treats 0 AD as identical to 1 AD."
@ivar origin: datetime instance defining the origin of the netCDF time variable.
@ivar calendar: the calendar used (as specified by the C{calendar} keyword).
@ivar unit_string: a string defining the the netCDF time variable.
@ivar units: the units part of C{unit_string} (i.e. 'days', 'hours', 'seconds').
"""
def __init__(self,unit_string,calendar='standard'):
"""
@param unit_string: a string of the form
C{'time-units since <time-origin>'} defining the time units.
Valid time-units are days, hours, minutes and seconds (the singular forms
are also accepted). An example unit_string would be C{'hours
since 0001-01-01 00:00:00'}.
@keyword calendar: describes the calendar used in the time calculations.
All the values currently defined in the U{CF metadata convention
<http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.1/cf-conventions.html#time-coordinate>}
are accepted. The default is C{'standard'}, which corresponds to the mixed
Gregorian/Julian calendar used by the C{udunits library}. Valid calendars
are:
- C{'gregorian'} or C{'standard'} (default):
Mixed Gregorian/Julian calendar as defined by udunits.
- C{'proleptic_gregorian'}:
A Gregorian calendar extended to dates before 1582-10-15. That is, a year
is a leap year if either (i) it is divisible by 4 but not by 100 or (ii)
it is divisible by 400.
- C{'noleap'} or C{'365_day'}:
Gregorian calendar without leap years, i.e., all years are 365 days long.
- C{'all_leap'} or C{'366_day'}:
Gregorian calendar with every year being a leap year, i.e.,
all years are 366 days long.
-C{'360_day'}:
All years are 360 days divided into 30 day months.
-C{'julian'}:
Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a
leap year if it is divisible by 4.
@returns: A class instance which may be used for converting times from netCDF
units to datetime objects.
"""
if calendar in _calendars:
self.calendar = calendar
else:
raise ValueError("calendar must be one of %s, got '%s'" % (str(_calendars),calendar))
units, tzoffset, self.origin = _dateparse(unit_string)
self.tzoffset = tzoffset # time zone offset in minutes
self.units = units
self.unit_string = unit_string
if self.calendar in ['noleap','365_day'] and self.origin.month == 2 and self.origin.day == 29:
raise ValueError('cannot specify a leap day as the reference time with the noleap calendar')
if self.calendar == '360_day' and self.origin.day > 30:
raise ValueError('there are only 30 days in every month with the 360_day calendar')
if self.calendar in ['noleap','365_day']:
self._jd0 = _NoLeapDayFromDate(self.origin)
elif self.calendar in ['all_leap','366_day']:
self._jd0 = _AllLeapFromDate(self.origin)
elif self.calendar == '360_day':
self._jd0 = _360DayFromDate(self.origin)
else:
self._jd0 = JulianDayFromDate(self.origin,calendar=self.calendar)
def date2num(self,date):
"""
Returns C{time_value} in units described by L{unit_string}, using
the specified L{calendar}, given a 'datetime-like' object.
The datetime object must represent UTC with no time-zone offset.
If there is a time-zone offset implied by L{unit_string}, it will
be applied to the returned numeric values.
Resolution is 1 second.
If C{calendar = 'standard'} or C{'gregorian'} (indicating
that the mixed Julian/Gregorian calendar is to be used), an
exception will be raised if the 'datetime-like' object describes
a date between 1582-10-5 and 1582-10-15.
Works for scalars, sequences and numpy arrays.
Returns a scalar if input is a scalar, else returns a numpy array.
"""
isscalar = False
try:
date[0]
except:
isscalar = True
if not isscalar:
date = numpy.array(date)
shape = date.shape
if self.calendar in ['julian','standard','gregorian','proleptic_gregorian']:
if isscalar:
jdelta = JulianDayFromDate(date,self.calendar)-self._jd0
else:
jdelta = [JulianDayFromDate(d,self.calendar)-self._jd0 for d in date.flat]
elif self.calendar in ['noleap','365_day']:
if isscalar:
if date.month == 2 and date.day == 29:
raise ValueError('there is no leap day in the noleap calendar')
jdelta = _NoLeapDayFromDate(date) - self._jd0
else:
jdelta = []
for d in date.flat:
if d.month == 2 and d.day == 29:
raise ValueError('there is no leap day in the noleap calendar')
jdelta.append(_NoLeapDayFromDate(d)-self._jd0)
elif self.calendar in ['all_leap','366_day']:
if isscalar:
jdelta = _AllLeapFromDate(date) - self._jd0
else:
jdelta = [_AllLeapFromDate(d)-self._jd0 for d in date.flat]
elif self.calendar == '360_day':
if isscalar:
if date.day > 30:
raise ValueError('there are only 30 days in every month with the 360_day calendar')
jdelta = _360DayFromDate(date) - self._jd0
else:
jdelta = []
for d in date.flat:
if d.day > 30:
raise ValueError('there are only 30 days in every month with the 360_day calendar')
jdelta.append(_360DayFromDate(d)-self._jd0)
if not isscalar:
jdelta = numpy.array(jdelta)
# convert to desired units, add time zone offset.
if self.units in ['second','seconds']:
jdelta = jdelta*86400. + self.tzoffset*60.
elif self.units in ['minute','minutes']:
jdelta = jdelta*1440. + self.tzoffset
elif self.units in ['hour','hours']:
jdelta = jdelta*24. + self.tzoffset/60.
elif self.units in ['day','days']:
jdelta = jdelta + self.tzoffset/1440.
if isscalar:
return jdelta
else:
return numpy.reshape(jdelta,shape)
def num2date(self,time_value):
"""
Return a 'datetime-like' object given a C{time_value} in units
described by L{unit_string}, using L{calendar}.
dates are in UTC with no offset, even if L{unit_string} contains
a time zone offset from UTC.
Resolution is 1 second.
Works for scalars, sequences and numpy arrays.
Returns a scalar if input is a scalar, else returns a numpy array.
The datetime instances returned by C{num2date} are 'real' python datetime
objects if the date falls in the Gregorian calendar (i.e.
C{calendar='proleptic_gregorian'}, or C{calendar = 'standard'/'gregorian'} and
the date is after 1582-10-15). Otherwise, they are 'phony' datetime
objects which are actually instances of netcdftime.datetime. This is
because the python datetime module cannot handle the weird dates in some
calendars (such as C{'360_day'} and C{'all_leap'}) which
do not exist in any real world calendar.
"""
isscalar = False
try:
time_value[0]
except:
isscalar = True
ismasked = False
if hasattr(time_value,'mask'):
mask = time_value.mask
ismasked = True
if not isscalar:
time_value = numpy.array(time_value, dtype='d')
shape = time_value.shape
# convert to desired units, remove time zone offset.
if self.units in ['second','seconds']:
jdelta = time_value/86400. - self.tzoffset/1440.
elif self.units in ['minute','minutes']:
jdelta = time_value/1440. - self.tzoffset/1440.
elif self.units in ['hour','hours']:
jdelta = time_value/24. - self.tzoffset/1440.
elif self.units in ['day','days']:
jdelta = time_value - self.tzoffset/1440.
jd = self._jd0 + jdelta
if self.calendar in ['julian','standard','gregorian','proleptic_gregorian']:
if not isscalar:
if ismasked:
date = []
for j,m in zip(jd.flat, mask.flat):
if not m:
date.append(DateFromJulianDay(j,self.calendar))
else:
date.append(None)
else:
date = [DateFromJulianDay(j,self.calendar) for j in jd.flat]
else:
if ismasked and mask.item():
date = None
else:
date = DateFromJulianDay(jd,self.calendar)
elif self.calendar in ['noleap','365_day']:
if not isscalar:
date = [_DateFromNoLeapDay(j) for j in jd.flat]
else:
date = _DateFromNoLeapDay(jd)
elif self.calendar in ['all_leap','366_day']:
if not isscalar:
date = [_DateFromAllLeap(j) for j in jd.flat]
else:
date = _DateFromAllLeap(jd)
elif self.calendar == '360_day':
if not isscalar:
date = [_DateFrom360Day(j) for j in jd.flat]
else:
date = _DateFrom360Day(jd)
if isscalar:
return date
else:
return numpy.reshape(numpy.array(date),shape)
def _parse_timezone(tzstring):
"""Parses ISO 8601 time zone specs into tzinfo offsets
Adapted from pyiso8601 (http://code.google.com/p/pyiso8601/)
"""
if tzstring == "Z":
return 0
# This isn't strictly correct, but it's common to encounter dates without
# timezones so I'll assume the default (which defaults to UTC).
if tzstring is None:
return 0
m = TIMEZONE_REGEX.match(tzstring)
prefix, hours, minutes = m.groups()
hours, minutes = int(hours), int(minutes)
if prefix == "-":
hours = -hours
minutes = -minutes
return minutes + hours*60.
def _parse_date(datestring):
"""Parses ISO 8601 dates into datetime objects
The timezone is parsed from the date string, assuming UTC
by default.
Adapted from pyiso8601 (http://code.google.com/p/pyiso8601/)
"""
if not isinstance(datestring, str) and not isinstance(datestring, unicode):
raise ValueError("Expecting a string %r" % datestring)
m = ISO8601_REGEX.match(datestring.strip())
if not m:
raise ValueError("Unable to parse date string %r" % datestring)
groups = m.groupdict()
tzoffset_mins = _parse_timezone(groups["timezone"])
if groups["hour"] is None:
groups["hour"]=0
if groups["minute"] is None:
groups["minute"]=0
if groups["second"] is None:
groups["second"]=0
#if groups["fraction"] is None:
# groups["fraction"] = 0
#else:
# groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6)
return int(groups["year"]), int(groups["month"]), int(groups["day"]),\
int(groups["hour"]), int(groups["minute"]), int(groups["second"]),\
tzoffset_mins
_illegal_s = re.compile(r"((^|[^%])(%%)*%s)")
def _findall(text, substr):
# Also finds overlaps
sites = []
i = 0
while 1:
j = text.find(substr, i)
if j == -1:
break
sites.append(j)
i=j+1
return sites
# Every 28 years the calendar repeats, except through century leap
# years where it's 6 years. But only if you're using the Gregorian
# calendar. ;)
def _strftime(dt, fmt):
if _illegal_s.search(fmt):
raise TypeError("This strftime implementation does not handle %s")
# don't use strftime method at all.
#if dt.year > 1900:
# return dt.strftime(fmt)
year = dt.year
# For every non-leap year century, advance by
# 6 years to get into the 28-year repeat cycle
delta = 2000 - year
off = 6*(delta // 100 + delta // 400)
year = year + off
# Move to around the year 2000
year = year + ((2000 - year)//28)*28
timetuple = dt.timetuple()
s1 = time.strftime(fmt, (year,) + timetuple[1:])
sites1 = _findall(s1, str(year))
s2 = time.strftime(fmt, (year+28,) + timetuple[1:])
sites2 = _findall(s2, str(year+28))
sites = []
for site in sites1:
if site in sites2:
sites.append(site)
s = s1
syear = "%4d" % (dt.year,)
for site in sites:
s = s[:site] + syear + s[site+4:]
return s
def date2num(dates,units,calendar='standard'):
"""
date2num(dates,units,calendar='standard')
Return numeric time values given datetime objects. The units
of the numeric time values are described by the L{units} argument
and the L{calendar} keyword. The datetime objects must
be in UTC with no time-zone offset. If there is a
time-zone offset in C{units}, it will be applied to the
returned numeric values.
Like the matplotlib C{date2num} function, except that it allows
for different units and calendars. Behaves the same if
C{units = 'days since 0001-01-01 00:00:00'} and
C{calendar = 'proleptic_gregorian'}.
@param dates: A datetime object or a sequence of datetime objects.
The datetime objects should not include a time-zone offset.
@param units: a string of the form C{'B{time units} since B{reference time}}'
describing the time units. B{C{time units}} can be days, hours, minutes
or seconds. B{C{reference time}} is the time origin. A valid choice
would be units=C{'hours since 1800-01-01 00:00:00 -6:00'}.
@param calendar: describes the calendar used in the time calculations.
All the values currently defined in the U{CF metadata convention
<http://cf-pcmdi.llnl.gov/documents/cf-conventions/>} are supported.
Valid calendars C{'standard', 'gregorian', 'proleptic_gregorian'
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'}.
Default is C{'standard'}, which is a mixed Julian/Gregorian calendar.
@return: a numeric time value, or an array of numeric time values.
The maximum resolution of the numeric time values is 1 second.
"""
cdftime = utime(units,calendar=calendar)
return cdftime.date2num(dates)
def num2date(times,units,calendar='standard'):
"""
num2date(times,units,calendar='standard')
Return datetime objects given numeric time values. The units
of the numeric time values are described by the C{units} argument
and the C{calendar} keyword. The returned datetime objects represent
UTC with no time-zone offset, even if the specified
C{units} contain a time-zone offset.
Like the matplotlib C{num2date} function, except that it allows
for different units and calendars. Behaves the same if
C{units = 'days since 001-01-01 00:00:00'} and
C{calendar = 'proleptic_gregorian'}.
@param times: numeric time values. Maximum resolution is 1 second.
@param units: a string of the form C{'B{time units} since B{reference time}}'
describing the time units. B{C{time units}} can be days, hours, minutes
or seconds. B{C{reference time}} is the time origin. A valid choice
would be units=C{'hours since 1800-01-01 00:00:00 -6:00'}.
@param calendar: describes the calendar used in the time calculations.
All the values currently defined in the U{CF metadata convention
<http://cf-pcmdi.llnl.gov/documents/cf-conventions/>} are supported.
Valid calendars C{'standard', 'gregorian', 'proleptic_gregorian'
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'}.
Default is C{'standard'}, which is a mixed Julian/Gregorian calendar.
@return: a datetime instance, or an array of datetime instances.
The datetime instances returned are 'real' python datetime
objects if the date falls in the Gregorian calendar (i.e.
C{calendar='proleptic_gregorian'}, or C{calendar = 'standard'} or C{'gregorian'}
and the date is after 1582-10-15). Otherwise, they are 'phony' datetime
objects which support some but not all the methods of 'real' python
datetime objects. This is because the python datetime module cannot
the uses the C{'proleptic_gregorian'} calendar, even before the switch
occured from the Julian calendar in 1582. The datetime instances
do not contain a time-zone offset, even if the specified C{units}
contains one.
"""
cdftime = utime(units,calendar=calendar)
return cdftime.num2date(times)
def _check_index(indices, times, nctime, calendar, select):
"""Return True if the time indices given correspond to the given times,
False otherwise.
Parameters:
indices : sequence of integers
Positive integers indexing the time variable.
times : sequence of times.
Reference times.
nctime : netCDF Variable object
NetCDF time object.
calendar : string
Calendar of nctime.