diff --git a/Changelog b/Changelog index 6e1a8092..f0f5fda6 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,5 @@ -version 1.4.0 (not yet released) -================================ +version 1.4.0 (release tag v1.4.0.rel) +====================================== * `cftime.date2num` will now always return an array of integers, if the units and times allow. Previously this would only be true if the units were 'microseconds' (PR #225). In other circumstances, as before, `cftime.date2num` @@ -7,6 +7,9 @@ version 1.4.0 (not yet released) * Rewrite of julian day/calendar functions (_IntJulianDayToCalendar and _IntJulianDayFromCalendar) to remove GPL'ed code. cftime license changed to MIT (to be consistent with netcdf4-python). + * Added datetime.toordinal() (returns julian day, kwarg 'fractional' + can be used to include fractional day). + * cftime.datetime no longer uses calendar-specific sub-classes. version 1.3.1 (release tag v1.3.1rel) ===================================== diff --git a/README.md b/README.md index 5871394a..edc72bed 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Time-handling functionality from netcdf4-python ## News For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog). +2/1/2021: Version 1.4.0 released. License changed to MIT (GPL'ed code replaced). +Roundtrip accuracy improved for units other than microseconds. Added +cftime.datetime.toordinal method, returns integer julian day number. + 1/17/2021: Version 1.3.1 released. 11/16/2020: Version 1.3.0 released. **API change**: The `cftime.datetime` constructor now creates diff --git a/docs/_build/html/_static/pygments.css b/docs/_build/html/_static/pygments.css index d14395ef..f346859c 100644 --- a/docs/_build/html/_static/pygments.css +++ b/docs/_build/html/_static/pygments.css @@ -1,8 +1,8 @@ pre { line-height: 125%; margin: 0; } -td.linenos pre { color: #000000; background-color: #f0f0f0; padding: 0 5px 0 5px; } -span.linenos { color: #000000; background-color: #f0f0f0; padding: 0 5px 0 5px; } -td.linenos pre.special { color: #000000; background-color: #ffffc0; padding: 0 5px 0 5px; } -span.linenos.special { color: #000000; background-color: #ffffc0; padding: 0 5px 0 5px; } +td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } +span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } +td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } .highlight .hll { background-color: #ffffcc } .highlight { background: #eeffcc; } .highlight .c { color: #408090; font-style: italic } /* Comment */ diff --git a/docs/_build/html/api.html b/docs/_build/html/api.html index d600c398..9b8468ea 100644 --- a/docs/_build/html/api.html +++ b/docs/_build/html/api.html @@ -226,6 +226,17 @@ day number within the current year starting with 1 for January 1st.

+
+
+toordinal(self)
+

Return julian day ordinal.

+

January 1 of the year -4713 is day 0 for the julian,gregorian and standard +calendars.

+

November 11 of the year -4714 is day 0 for the proleptic gregorian calendar.

+

January 1 of the year zero is day 0 for the 360_day, 365_day, 366_day and +no_leap calendars.

+
+
diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html index b84a0578..22068a96 100644 --- a/docs/_build/html/genindex.html +++ b/docs/_build/html/genindex.html @@ -150,6 +150,8 @@

T

diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv index 5f62e642..e6d8e0c2 100644 --- a/docs/_build/html/objects.inv +++ b/docs/_build/html/objects.inv @@ -2,4 +2,7 @@ # Project: cftime # Version: # The remainder of this file is compressed using zlib. -xڕJ0}^[t{[XO0&60!IIcVKHf/&iyBf2b$dw l&G[VW|ƚ3|pF=$AaJb7j~U8ir"zF{)B~5)uɋPjWڦS"b~zTrռp3NA=š%X.֧Wd!ؖ(yBTa pV9vj/U: xGJ%MD rKtn^C*/۩^y\UvGkx)zk{LJcyz>Q \ No newline at end of file +xڕj y +&{ҲK[}&h޾fSJ^DgOQ 2;!{``eEw9R߳!3<}1A fI M'~vO)rMB$/C]iNY +A-W5қq +.0Fl)-r<"# atbRm*;QvM c!; Yء=U')!s4s4{+"Hnyi *ozNd5֎Rk1Dz-tX}L. \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js index 21f7f436..ea0a5f02 100644 --- a/docs/_build/html/searchindex.js +++ b/docs/_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["api","index","installing"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,sphinx:56},filenames:["api.rst","index.rst","installing.rst"],objects:{"":{cftime:[0,0,0,"-"]},"cftime.datetime":{isoformat:[0,3,1,""],replace:[0,3,1,""],strftime:[0,3,1,""],timetuple:[0,3,1,""]},cftime:{DateFromJulianDay:[0,1,1,""],DatetimeAllLeap:[0,2,1,""],DatetimeGregorian:[0,2,1,""],DatetimeJulian:[0,2,1,""],DatetimeNoLeap:[0,2,1,""],DatetimeProlepticGregorian:[0,2,1,""],JulianDayFromDate:[0,1,1,""],date2index:[0,1,1,""],date2num:[0,1,1,""],datetime:[0,2,1,""],num2date:[0,1,1,""],num2pydate:[0,1,1,""],time2index:[0,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method"},terms:{"100":0,"1582":0,"1st":0,"360_dai":0,"365_dai":0,"366_dai":0,"class":0,"default":[0,2],"int":0,"new":[0,2],"return":0,"true":0,For:0,Has:0,The:[0,2],Then:2,__add__:0,__repr__:0,__str__:0,__sub__:0,_cftime:0,accuraci:0,after:0,all:0,all_leap:0,allow:0,also:2,alwai:0,api:1,appear:2,appli:0,approxim:0,arg:0,argument:0,arrai:0,associ:0,assum:0,attribut:0,auto:0,awar:0,base:0,befor:[0,2],behavior:0,being:2,between:0,blank:0,breakpoint:0,build:2,build_ext:2,calcul:0,calendar:0,can:0,cannot:0,cartopi:2,cfconvent:0,cftime:[0,2],chang:2,channel:2,check:2,climat:1,clone:2,closest:0,command:2,commun:2,compar:0,comparison:0,complet:0,comput:0,conda:2,conform:1,contain:0,control:0,convent:[0,1],correspond:0,creat:0,ctime:0,current:0,cython:2,dai:0,date2index:0,date2num:0,date:0,datefromjuliandai:0,datetim:0,datetimeallleap:0,datetimegregorian:0,datetimejulian:0,datetimenoleap:0,datetimeprolepticgregorian:0,dayofwk:0,dayofyr:0,daysinmonth:0,decod:1,defin:0,depend:1,describ:0,develop:1,differ:0,difficult:2,direct:0,document:0,don:[0,2],dst:0,easiest:2,either:0,entri:0,equival:0,error:0,even:0,everyth:2,exact:0,exist:0,explicit:0,extens:2,fall:0,fals:0,field:0,file:1,first:2,flag:0,follow:0,forecast:1,forg:2,form:0,format:0,found:0,fraction:0,from:0,get:2,github:2,given:0,gregorian:0,have:[0,2],hour:0,http:0,ignor:0,includ:0,increas:0,index:[0,1],indic:0,inplac:2,input:0,instal:1,instanc:0,instruct:1,isoformat:0,its:0,januari:0,julian:0,juliandayfromd:0,just:0,keyword:0,kwarg:0,last:0,later:2,librari:1,like:0,line:2,list:0,localtim:0,mai:2,maintain:2,match:0,mean:0,metadata:0,method:0,microsecond:0,millisecond:0,mimic:0,minut:0,mix:0,modul:1,month:0,months_sinc:0,must:0,nativ:0,nctime:0,nearest:0,need:2,netcdf:[0,1],noleap:0,none:0,note:0,num2dat:0,num2pyd:0,number:0,numer:0,numpi:2,object:0,offset:0,one:0,onli:0,only_use_cftime_datetim:0,only_use_python_datetim:0,oper:0,order:0,org:0,origin:0,other:0,otherwis:0,overload:0,page:1,pass:2,perfectli:0,phoni:0,pip:2,place:2,possibl:0,produc:0,prolept:0,proleptic_gregorian:0,pyarg:2,pynio:2,pytest:2,python:[0,1,2],rais:0,real:0,recommend:2,refer:0,releas:2,replac:0,repositori:2,repres:0,requir:1,return_tupl:0,rist:0,run:2,same:0,search:1,second:0,section:0,see:0,select:0,self:0,sep:0,sequenc:0,set:0,setup:2,should:0,sinc:0,some:0,specifi:0,standard:0,start:0,store:0,strftime:0,string:0,strptime:0,struct_tim:0,subclass:0,subtract:0,suit:2,support:0,sure:2,synonym:0,test:2,than:0,thei:0,thi:0,time2index:0,time:[0,1],timedelta:0,timespec:0,timetupl:0,tool:2,unit:[0,1],unless:0,updat:2,use:[0,2],use_only_python_datetim:0,used:0,uses:0,using:[0,2],utc:0,valid:0,valu:[0,1],variabl:[0,1],versa:0,vice:0,wai:2,weekdai:0,when:2,where:0,which:0,within:0,work:0,ydai:0,year:0,you:2,zone:0},titles:["API","cftime","Installation"],titleterms:{api:0,cftime:1,content:1,depend:2,develop:2,indic:1,instal:2,instruct:2,requir:2,tabl:1}}) \ No newline at end of file +Search.setIndex({docnames:["api","index","installing"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,sphinx:56},filenames:["api.rst","index.rst","installing.rst"],objects:{"":{cftime:[0,0,0,"-"]},"cftime.datetime":{isoformat:[0,3,1,""],replace:[0,3,1,""],strftime:[0,3,1,""],timetuple:[0,3,1,""],toordinal:[0,3,1,""]},cftime:{DateFromJulianDay:[0,1,1,""],DatetimeAllLeap:[0,2,1,""],DatetimeGregorian:[0,2,1,""],DatetimeJulian:[0,2,1,""],DatetimeNoLeap:[0,2,1,""],DatetimeProlepticGregorian:[0,2,1,""],JulianDayFromDate:[0,1,1,""],date2index:[0,1,1,""],date2num:[0,1,1,""],datetime:[0,2,1,""],num2date:[0,1,1,""],num2pydate:[0,1,1,""],time2index:[0,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method"},terms:{"100":0,"1582":0,"1st":0,"360_dai":0,"365_dai":0,"366_dai":0,"4713":0,"4714":0,"class":0,"default":[0,2],"int":0,"new":[0,2],"return":0,"true":0,For:0,Has:0,The:[0,2],Then:2,__add__:0,__repr__:0,__str__:0,__sub__:0,_cftime:0,accuraci:0,after:0,all:0,all_leap:0,allow:0,also:2,alwai:0,api:1,appear:2,appli:0,approxim:0,arg:0,argument:0,arrai:0,associ:0,assum:0,attribut:0,auto:0,awar:0,base:0,befor:[0,2],behavior:0,being:2,between:0,blank:0,breakpoint:0,build:2,build_ext:2,calcul:0,calendar:0,can:0,cannot:0,cartopi:2,cfconvent:0,cftime:[0,2],chang:2,channel:2,check:2,climat:1,clone:2,closest:0,command:2,commun:2,compar:0,comparison:0,complet:0,comput:0,conda:2,conform:1,contain:0,control:0,convent:[0,1],correspond:0,creat:0,ctime:0,current:0,cython:2,dai:0,date2index:0,date2num:0,date:0,datefromjuliandai:0,datetim:0,datetimeallleap:0,datetimegregorian:0,datetimejulian:0,datetimenoleap:0,datetimeprolepticgregorian:0,dayofwk:0,dayofyr:0,daysinmonth:0,decod:1,defin:0,depend:1,describ:0,develop:1,differ:0,difficult:2,direct:0,document:0,don:[0,2],dst:0,easiest:2,either:0,entri:0,equival:0,error:0,even:0,everyth:2,exact:0,exist:0,explicit:0,extens:2,fall:0,fals:0,field:0,file:1,first:2,flag:0,follow:0,forecast:1,forg:2,form:0,format:0,found:0,fraction:0,from:0,get:2,github:2,given:0,gregorian:0,have:[0,2],hour:0,http:0,ignor:0,includ:0,increas:0,index:[0,1],indic:0,inplac:2,input:0,instal:1,instanc:0,instruct:1,isoformat:0,its:0,januari:0,julian:0,juliandayfromd:0,just:0,keyword:0,kwarg:0,last:0,later:2,librari:1,like:0,line:2,list:0,localtim:0,mai:2,maintain:2,match:0,mean:0,metadata:0,method:0,microsecond:0,millisecond:0,mimic:0,minut:0,mix:0,modul:1,month:0,months_sinc:0,must:0,nativ:0,nctime:0,nearest:0,need:2,netcdf:[0,1],no_leap:0,noleap:0,none:0,note:0,novemb:0,num2dat:0,num2pyd:0,number:0,numer:0,numpi:2,object:0,offset:0,one:0,onli:0,only_use_cftime_datetim:0,only_use_python_datetim:0,oper:0,order:0,ordin:0,org:0,origin:0,other:0,otherwis:0,overload:0,page:1,pass:2,perfectli:0,phoni:0,pip:2,place:2,possibl:0,produc:0,prolept:0,proleptic_gregorian:0,pyarg:2,pynio:2,pytest:2,python:[0,1,2],rais:0,real:0,recommend:2,refer:0,releas:2,replac:0,repositori:2,repres:0,requir:1,return_tupl:0,rist:0,run:2,same:0,search:1,second:0,section:0,see:0,select:0,self:0,sep:0,sequenc:0,set:0,setup:2,should:0,sinc:0,some:0,specifi:0,standard:0,start:0,store:0,strftime:0,string:0,strptime:0,struct_tim:0,subclass:0,subtract:0,suit:2,support:0,sure:2,synonym:0,test:2,than:0,thei:0,thi:0,time2index:0,time:[0,1],timedelta:0,timespec:0,timetupl:0,tool:2,toordin:0,unit:[0,1],unless:0,updat:2,use:[0,2],use_only_python_datetim:0,used:0,uses:0,using:[0,2],utc:0,valid:0,valu:[0,1],variabl:[0,1],versa:0,vice:0,wai:2,weekdai:0,when:2,where:0,which:0,within:0,work:0,ydai:0,year:0,you:2,zero:0,zone:0},titles:["API","cftime","Installation"],titleterms:{api:0,cftime:1,content:1,depend:2,develop:2,indic:1,instal:2,instruct:2,requir:2,tabl:1}}) \ No newline at end of file diff --git a/setup.py b/setup.py index 79f195db..32681581 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ SRCDIR = os.path.join(BASEDIR,'src') CMDS_NOCYTHONIZE = ['clean','clean_cython','sdist'] COMPILER_DIRECTIVES = {} -DEFINE_MACROS = None +DEFINE_MACROS = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")] FLAG_COVERAGE = '--cython-coverage' # custom flag enabling Cython line tracing NAME = 'cftime' CFTIME_DIR = os.path.join(SRCDIR, NAME) @@ -78,7 +78,7 @@ def description(): 'warn.maybe_uninitialized': False, 'warn.unreachable': False, 'warn.unused': False} - DEFINE_MACROS = [('CYTHON_TRACE', '1'), + DEFINE_MACROS += [('CYTHON_TRACE', '1'), ('CYTHON_TRACE_NOGIL', '1')] if FLAG_COVERAGE in sys.argv: sys.argv.remove(FLAG_COVERAGE) diff --git a/src/cftime/__init__.py b/src/cftime/__init__.py index cbef82b2..30e8e721 100644 --- a/src/cftime/__init__.py +++ b/src/cftime/__init__.py @@ -1,8 +1,10 @@ -from ._cftime import utime, JulianDayFromDate, DateFromJulianDay, UNIT_CONVERSION_FACTORS -from ._cftime import _parse_date, date2index, time2index, datetime, real_datetime -from ._cftime import DatetimeNoLeap, DatetimeAllLeap, Datetime360Day, DatetimeJulian, \ - DatetimeGregorian, DatetimeProlepticGregorian +from ._cftime import datetime, real_datetime, _parse_date +from ._cftime import num2date, date2num, date2index, time2index, num2pydate from ._cftime import microsec_units, millisec_units, \ - sec_units, hr_units, day_units, min_units -from ._cftime import num2date, date2num, date2index, num2pydate + sec_units, hr_units, day_units, min_units,\ + UNIT_CONVERSION_FACTORS from ._cftime import __version__ +# legacy functions in _cftime_legacy.pyx +from ._cftime import DatetimeNoLeap, DatetimeAllLeap, Datetime360Day, DatetimeJulian, \ + DatetimeGregorian, DatetimeProlepticGregorian +from ._cftime import utime, JulianDayFromDate, DateFromJulianDay diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index c7e92ba5..86af3921 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -37,13 +37,6 @@ cdef int[12] _dayspermonth_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 3 cdef int[13] _cumdayspermonth = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365] cdef int[13] _cumdayspermonth_leap = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366] -# Slightly more performant cython lookups than a 2D table -# The first 12 entries correspond to month lengths for non-leap years. -# The remaining 12 entries give month lengths for leap years -cdef int32_t* days_per_month_array = [ - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, - 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - # Reverse operator lookup for datetime.__richcmp__ _rop_lookup = {Py_LT: '__gt__', Py_LE: '__ge__', Py_EQ: '__eq__', Py_GT: '__lt__', Py_GE: '__le__', Py_NE: '__ne__'} @@ -62,29 +55,6 @@ ISO8601_REGEX = re.compile(r"(?P[+-]?[0-9]+)(-(?P[0-9]{1,2})(-(?P[+-])(?P[0-9]{2})(?:(?::(?P[0-9]{2}))|(?P[0-9]{2}))?") - -# Taken from pandas ccalendar.pyx -@cython.wraparound(False) -@cython.boundscheck(False) -cpdef int32_t get_days_in_month(bint isleap, int month) nogil: - """ - Return the number of days in the given month of the given year. - Parameters - ---------- - leap : int [0,1] - month : int - - Returns - ------- - days_in_month : int - Notes - ----- - Assumes that the arguments are valid. Passing a month not between 1 and 12 - risks a segfault. - """ - return days_per_month_array[12 * isleap + month - 1] - - class real_datetime(datetime_python): """add dayofwk, dayofyr, daysinmonth attributes to python datetime instance""" @property @@ -96,7 +66,10 @@ class real_datetime(datetime_python): return self.timetuple().tm_yday @property def daysinmonth(self): - return get_days_in_month(_is_leap(self.year,'proleptic_gregorian'), self.month) + if _is_leap(self.year,'proleptic_gregorian'): + return _dayspermonth_leap[self.month-1] + else: + return _dayspermonth[self.month-1] nanosecond = 0 # workaround for pandas bug (cftime issue #77) def _datesplit(timestr): @@ -322,58 +295,26 @@ UNIT_CONVERSION_FACTORS = { "months": 30 * 86400 * 1000000 } - -DATE_TYPES = { - "proleptic_gregorian": DatetimeProlepticGregorian, - "standard": DatetimeGregorian, - "noleap": DatetimeNoLeap, - "365_day": DatetimeNoLeap, - "all_leap": DatetimeAllLeap, - "366_day": DatetimeAllLeap, - "julian": DatetimeJulian, - "360_day": Datetime360Day, - "gregorian": DatetimeGregorian -} - - -#def to_calendar_specific_datetime(dt, calendar, use_python_datetime): -# if use_python_datetime: -# return real_datetime( -# dt.year, -# dt.month, -# dt.day, -# dt.hour, -# dt.minute, -# dt.second, -# dt.microsecond) -# else: -# return datetime( -# dt.year, -# dt.month, -# dt.day, -# dt.hour, -# dt.minute, -# dt.second, -# dt.microsecond, -# calendar=calendar) -# return calendar-specific subclasses for backward compatbility, -# even though after 1.3.0 this is no longer necessary. def to_calendar_specific_datetime(dt, calendar, use_python_datetime): if use_python_datetime: - date_type = real_datetime + return real_datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond) else: - date_type = DATE_TYPES[calendar] - - return date_type( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond - ) - + return datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + calendar=calendar) _MAX_INT64 = np.iinfo("int64").max _MIN_INT64 = np.iinfo("int64").min @@ -979,13 +920,13 @@ The default format of the string produced by strftime is controlled by self.form @property def dayofwk(self): if self._dayofwk < 0 and self.calendar: - jd = _IntJulianDayFromDate(self.year,self.month,self.day,self.calendar, - skip_transition=False,has_year_zero=self.has_year_zero) - year,month,day,dayofwk,dayofyr = _IntJulianDayToDate(jd,self.calendar, - skip_transition=False,has_year_zero=self.has_year_zero) - # cache results for dayofwk, dayofyr + jd = self.toordinal() + dayofwk = (jd + 1) % 7 + # convert to ISO 8601 (0 = Monday, 6 = Sunday), like python datetime + dayofwk -= 1 + if dayofwk == -1: dayofwk = 6 + # cache results for dayofwk self._dayofwk = dayofwk - self._dayofyr = dayofyr return dayofwk else: return self._dayofwk @@ -993,12 +934,14 @@ The default format of the string produced by strftime is controlled by self.form @property def dayofyr(self): if self._dayofyr < 0 and self.calendar: - jd = _IntJulianDayFromDate(self.year,self.month,self.day,self.calendar, - skip_transition=False,has_year_zero=self.has_year_zero) - year,month,day,dayofwk,dayofyr = _IntJulianDayToDate(jd,self.calendar, - skip_transition=False,has_year_zero=self.has_year_zero) - # cache results for dayofwk, dayofyr - self._dayofwk = dayofwk + if self.calendar == '360_day': + dayofyr = (self.month-1)*30+self.day + else: + if _is_leap(self.year,self.calendar,has_year_zero=self.has_year_zero): + dayofyr = _cumdayspermonth_leap[self.month-1]+self.day + else: + dayofyr = _cumdayspermonth[self.month-1]+self.day + # cache results for dayofyr self._dayofyr = dayofyr return dayofyr else: @@ -1006,15 +949,14 @@ The default format of the string produced by strftime is controlled by self.form @property def daysinmonth(self): - if self.calendar == 'noleap': - return _dayspermonth[self.month-1] - elif self.calendar == 'all_leap': - return _dayspermonth_leap[self.month-1] - elif self.calendar == '360_day': + if self.calendar == '360_day': return 30 else: - return get_days_in_month(_is_leap(self.year,self.calendar, - has_year_zero=self.has_year_zero), self.month) + if _is_leap(self.year,self.calendar, + has_year_zero=self.has_year_zero): + return _dayspermonth_leap[self.month-1] + else: + return _dayspermonth[self.month-1] def strftime(self, format=None): """ @@ -1138,7 +1080,7 @@ The default format of the string produced by strftime is controlled by self.form cdef _getstate(self): return (self.year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, - self._dayofwk, self._dayofyr) + self._dayofwk, self._dayofyr, self.calendar) def __reduce__(self): """special method that allows instance to be pickled""" @@ -1147,6 +1089,31 @@ The default format of the string produced by strftime is controlled by self.form cdef _add_timedelta(self, other): return NotImplemented + def toordinal(self,fractional=False): + """Return julian day ordinal. + + January 1 of the year -4713 is day 0 for the julian,gregorian and standard + calendars. + + November 11 of the year -4714 is day 0 for the proleptic gregorian calendar. + + January 1 of the year zero is day 0 for the 360_day, 365_day, 366_day and + no_leap calendars. + + If fractional=True, fractional part of day is included (default + False).""" + ijd = _IntJulianDayFromDate(self.year, self.month, self.day, self.calendar, + skip_transition=False,has_year_zero=self.has_year_zero) + if fractional: + fracday = self.hour / 24.0 + self.minute / 1440.0 + (self.second + + self.microsecond/1.e6) / 86400.0 + # at this point jd is an integer representing noon UTC on the given + # year,month,day. + # compute fractional day from hour,minute,second,microsecond + return ijd - 0.5 + fracday + else: + return ijd + def __add__(self, other): cdef datetime dt if isinstance(self, datetime) and isinstance(other, timedelta): @@ -1162,23 +1129,17 @@ The default format of the string produced by strftime is controlled by self.form # return calendar-specific subclasses for backward compatbility, # even though after 1.3.0 this is no longer necessary. if calendar == '360_day': - #return dt.__class__(*add_timedelta_360_day(dt, delta),calendar=calendar) - return Datetime360Day(*add_timedelta_360_day(dt, delta)) + return dt.__class__(*add_timedelta_360_day(dt, delta),calendar=calendar) elif calendar == 'noleap': - #return dt.__class__(*add_timedelta(dt, delta, no_leap, False, True),calendar=calendar) - return DatetimeNoLeap(*add_timedelta(dt, delta, no_leap, False, True)) + return dt.__class__(*add_timedelta(dt, delta, no_leap, False, True),calendar=calendar) elif calendar == 'all_leap': - #return dt.__class__(*add_timedelta(dt, delta, all_leap, False, True),calendar=calendar) - return DatetimeAllLeap(*add_timedelta(dt, delta, all_leap, False, True)) + return dt.__class__(*add_timedelta(dt, delta, all_leap, False, True),calendar=calendar) elif calendar == 'julian': - #return dt.__class__(*add_timedelta(dt, delta, is_leap_julian, False, False),calendar=calendar) - return DatetimeJulian(*add_timedelta(dt, delta, is_leap_julian, False, False)) + return dt.__class__(*add_timedelta(dt, delta, is_leap_julian, False, False),calendar=calendar) elif calendar == 'gregorian': - #return dt.__class__(*add_timedelta(dt, delta, is_leap_gregorian, True, False),calendar=calendar) - return DatetimeGregorian(*add_timedelta(dt, delta, is_leap_gregorian, True, False)) + return dt.__class__(*add_timedelta(dt, delta, is_leap_gregorian, True, False),calendar=calendar) elif calendar == 'proleptic_gregorian': - #return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, False),calendar=calendar) - return DatetimeProlepticGregorian(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, False)) + return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, False),calendar=calendar) else: return NotImplemented @@ -1192,10 +1153,8 @@ The default format of the string produced by strftime is controlled by self.form raise ValueError("cannot compute the time difference between dates with different calendars") if dt.calendar == "": raise ValueError("cannot compute the time difference between dates that are not calendar-aware") - ordinal_self = _IntJulianDayFromDate(dt.year, dt.month, dt.day, dt.calendar, - skip_transition=False,has_year_zero=self.has_year_zero) - ordinal_other = _IntJulianDayFromDate(other.year, other.month, other.day, other.calendar, - skip_transition=False,has_year_zero=self.has_year_zero) + ordinal_self = self.toordinal() # julian day + ordinal_other = other.toordinal() days = ordinal_self - ordinal_other seconds_self = dt.second + 60 * dt.minute + 3600 * dt.hour seconds_other = other.second + 60 * other.minute + 3600 * other.hour @@ -1217,24 +1176,18 @@ datetime object.""" # return calendar-specific subclasses for backward compatbility, # even though after 1.3.0 this is no longer necessary. if self.calendar == '360_day': - #return self.__class__(*add_timedelta_360_day(self, -other),calendar=self.calendar) - return Datetime360Day(*add_timedelta_360_day(self, -other)) + return self.__class__(*add_timedelta_360_day(self, -other),calendar=self.calendar) elif self.calendar == 'noleap': - #return self.__class__(*add_timedelta(self, -other, no_leap, False, True),calendar=self.calendar) - return DatetimeNoLeap(*add_timedelta(self, -other, no_leap, False, True)) + return self.__class__(*add_timedelta(self, -other, no_leap, False, True),calendar=self.calendar) elif self.calendar == 'all_leap': - #return self.__class__(*add_timedelta(self, -other, all_leap, False, True),calendar=self.calendar) - return DatetimeAllLeap(*add_timedelta(self, -other, all_leap, False, True)) + return self.__class__(*add_timedelta(self, -other, all_leap, False, True),calendar=self.calendar) elif self.calendar == 'julian': - #return self.__class__(*add_timedelta(self, -other, is_leap_julian, False, False),calendar=self.calendar) - return DatetimeJulian(*add_timedelta(self, -other, is_leap_julian, False, False)) + return self.__class__(*add_timedelta(self, -other, is_leap_julian, False, False),calendar=self.calendar) elif self.calendar == 'gregorian': - #return self.__class__(*add_timedelta(self, -other, is_leap_gregorian, True, False),calendar=self.calendar) - return DatetimeGregorian(*add_timedelta(self, -other, is_leap_gregorian, True, False)) + return self.__class__(*add_timedelta(self, -other, is_leap_gregorian, True, False),calendar=self.calendar) elif self.calendar == 'proleptic_gregorian': - #return self.__class__(*add_timedelta(self, -other, - # is_leap_proleptic_gregorian, False, False),calendar=self.calendar) - return DatetimeProlepticGregorian(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, False)) + return self.__class__(*add_timedelta(self, -other, + is_leap_proleptic_gregorian, False, False),calendar=self.calendar) else: return NotImplemented else: @@ -1253,113 +1206,6 @@ datetime object.""" else: return NotImplemented -# these calendar-specific sub-classes are no longer used, but stubs -# remain for backward compatibility. - -@cython.embedsignature(True) -cdef class DatetimeNoLeap(datetime): - """ -Phony datetime object which mimics the python datetime object, -but uses the "noleap" ("365_day") calendar. - """ - def __init__(self, *args, **kwargs): - kwargs['calendar']='noleap' - super().__init__(*args, **kwargs) - def __repr__(self): - return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', - self.__class__.__name__, - self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) - -@cython.embedsignature(True) -cdef class DatetimeAllLeap(datetime): - """ -Phony datetime object which mimics the python datetime object, -but uses the "all_leap" ("366_day") calendar. - """ - def __init__(self, *args, **kwargs): - kwargs['calendar']='all_leap' - super().__init__(*args, **kwargs) - def __repr__(self): - return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', - self.__class__.__name__, - self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) - -@cython.embedsignature(True) -cdef class Datetime360Day(datetime): - """ -Phony datetime object which mimics the python datetime object, -but uses the "360_day" calendar. - """ - def __init__(self, *args, **kwargs): - kwargs['calendar']='360_day' - super().__init__(*args, **kwargs) - def __repr__(self): - return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', - self.__class__.__name__, - self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) - -@cython.embedsignature(True) -cdef class DatetimeJulian(datetime): - """ -Phony datetime object which mimics the python datetime object, -but uses the "julian" calendar. - """ - def __init__(self, *args, **kwargs): - kwargs['calendar']='julian' - super().__init__(*args, **kwargs) - def __repr__(self): - return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', - self.__class__.__name__, - self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) - -@cython.embedsignature(True) -cdef class DatetimeGregorian(datetime): - """ -Phony datetime object which mimics the python datetime object, -but uses the mixed Julian-Gregorian ("standard", "gregorian") calendar. - -The last date of the Julian calendar is 1582-10-4, which is followed -by 1582-10-15, using the Gregorian calendar. - -Instances using the date after 1582-10-15 can be compared to -datetime.datetime instances and used to compute time differences -(datetime.timedelta) by subtracting a DatetimeGregorian instance from -a datetime.datetime instance or vice versa. - """ - def __init__(self, *args, **kwargs): - kwargs['calendar']='gregorian' - super().__init__(*args, **kwargs) - def __repr__(self): - return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', - self.__class__.__name__, - self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) - -@cython.embedsignature(True) -cdef class DatetimeProlepticGregorian(datetime): - """ -Phony datetime object which mimics the python datetime object, -but allows for dates that don't exist in the proleptic gregorian calendar. - -Supports timedelta operations by overloading + and -. - -Has strftime, timetuple, replace, __repr__, and __str__ methods. The -format of the string produced by __str__ is controlled by self.format -(default %Y-%m-%d %H:%M:%S). Supports comparisons with other -datetime instances using the same calendar; comparison with -native python datetime instances is possible for cftime.datetime -instances using 'gregorian' and 'proleptic_gregorian' calendars. - -Instance variables are year,month,day,hour,minute,second,microsecond,dayofwk,dayofyr, -format, and calendar. - """ - def __init__(self, *args, **kwargs): - kwargs['calendar']='proleptic_gregorian' - super().__init__( *args, **kwargs) - def __repr__(self): - return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', - self.__class__.__name__, - self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) - _illegal_s = re.compile(r"((^|[^%])(%%)*%s)") @@ -1649,7 +1495,7 @@ cdef _check_calendar(calendar): calout = '366_day' return calout -# The following functions (_IntJulianDayFromDate and _IntJulianDayToDate) are based on +# The following function (_IntJulianDayFromDate) is based on # algorithms described in the book # "Calendrical Calculations" by Dershowitz and Rheingold, 3rd edition, Cambridge University Press, 2007 # and the C implementation provided at https://reingold.co/calendar.C @@ -1748,504 +1594,5 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F else: return jday_greg -cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False,has_year_zero=False): - """Compute the year,month,day,dow,doy given the integer Julian day. - and calendar. (dow = day of week with 0=Mon,6=Sun and doy is day of year). - - Allowed calendars are 'standard', 'gregorian', 'julian', - 'proleptic_gregorian','360_day', '365_day', '366_day', 'noleap', - 'all_leap'. - - 'noleap' is a synonym for '365_day' - 'all_leap' is a synonym for '366_day' - 'gregorian' is a synonym for 'standard' - - optional kwarg 'skip_transition': When True, assume a 10-day - gap in Julian day numbers between Oct 4 and Oct 15 1582 (the transition - from Julian to Gregorian calendars). Default False, ignored - unless calendar = 'standard'.""" - cdef int year,month,day,dow,doy,yp1,jday_count,nextra - cdef int[12] dayspermonth - cdef int[13] cumdayspermonth - - # validate inputs. - calendar = _check_calendar(calendar) - - # compute day of week. - dow = (jday + 1) % 7 - # convert to ISO 8601 (0 = Monday, 6 = Sunday), like python datetime - dow -= 1 - if dow == -1: dow = 6 - - # handle all calendars except standard, julian, proleptic_gregorian. - if calendar == '360_day': - year = jday//360 - nextra = jday - year*360 - doy = nextra + 1 # Julday numbering starts at 0, doy starts at 1 - month = nextra//30 + 1 - day = doy - (month-1)*30 - return year,month,day,dow,doy - elif calendar == '365_day': - year = jday//365 - nextra = jday - year*365 - doy = nextra + 1 # Julday numbering starts at 0, doy starts at 1 - month = 1 - while doy > _cumdayspermonth[month]: - month += 1 - day = doy - _cumdayspermonth[month-1] - return year,month,day,dow,doy - elif calendar == '366_day': - year = jday//366 - nextra = jday - year*366 - doy = nextra + 1 # Julday numbering starts at 0, doy starts at 1 - month = 1 - while doy > _cumdayspermonth_leap[month]: - month += 1 - day = doy - _cumdayspermonth_leap[month-1] - return year,month,day,dow,doy - - # handle standard, julian, proleptic_gregorian calendars. - if jday < 0: - raise ValueError('julian day must be a positive integer') - - # start with initial guess of year that is before jday=1 in both - # Julian and Gregorian calendars. - year = jday//366 - 4714 - - # account for 10 days in Julian/Gregorian transition. - if not skip_transition and calendar == 'standard' and jday > 2299160: - jday += 10 - - yp1 = year + 1 - if yp1 == 0 and not has_year_zero: - yp1 = 1 # no year 0 - # initialize jday_count to Jan 1 of next year - jday_count = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero) - # Advance years until we find the right one - # (stop iteration when jday_count jday >= specified jday) - while jday >= jday_count: - year += 1 - if year == 0 and not has_year_zero: - year = 1 - yp1 = year + 1 - if yp1 == 0 and not has_year_zero: - yp1 = 1 - jday_count = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero) - # now we know year. - # set days in specified month, cumulative days in computed year. - if _is_leap(year, calendar,has_year_zero=has_year_zero): - dayspermonth = _dayspermonth_leap - cumdayspermonth = _cumdayspermonth_leap - else: - dayspermonth = _dayspermonth - cumdayspermonth = _cumdayspermonth - # initialized month to Jan, initialize jday_count to end of Jan of - # calculated year. - month = 1 - jday_count =\ - _IntJulianDayFromDate(year,month,dayspermonth[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero) - # now iterate by month until jday_count >= specified jday - while jday > jday_count: - month += 1 - jday_count =\ - _IntJulianDayFromDate(year,month,dayspermonth[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero) - # back up jday_count to 1st day of computed month - jday_count = _IntJulianDayFromDate(year,month,1,calendar,skip_transition=True,has_year_zero=has_year_zero) - # now jday_count represents day 1 of computed month in computed year - # so computed day is just difference between jday_count and specified jday. - day = jday - jday_count + 1 - # compute day in specified year. - if month == 1: - doy = day - else: - doy = cumdayspermonth[month-1]+day - return year,month,day,dow,doy - -# stuff below no longer used, kept here for backwards compatibility. - -def _round_half_up(x): - # 'round half up' so 0.5 rounded to 1 (instead of 0 as in numpy.round) - return np.ceil(np.floor(2.*x)/2.) - -@cython.embedsignature(True) -def JulianDayFromDate(date, calendar='standard'): - """JulianDayFromDate(date, calendar='standard') - - creates a Julian Day from a 'datetime-like' object. Returns the fractional - Julian Day (approximately 100 microsecond accuracy). - - 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. - """ - - # check if input was scalar and change return accordingly - isscalar = False - try: - date[0] - except: - isscalar = True - - date = np.atleast_1d(np.array(date)) - year = np.empty(len(date), dtype=np.int32) - month = year.copy() - day = year.copy() - hour = year.copy() - minute = year.copy() - second = year.copy() - microsecond = year.copy() - jd = np.empty(year.shape, np.longdouble) - cdef long double[:] jd_view = jd - cdef Py_ssize_t i_max = len(date) - cdef Py_ssize_t i - for i in range(i_max): - d = date[i] - if getattr(d, 'tzinfo', None) is not None: - d = d.replace(tzinfo=None) - d.utcoffset() - - year[i] = d.year - month[i] = d.month - day[i] = d.day - hour[i] = d.hour - minute[i] = d.minute - second[i] = d.second - microsecond[i] = d.microsecond - jd_view[i] = _IntJulianDayFromDate(year[i],month[i],day[i],calendar) - - # at this point jd is an integer representing noon UTC on the given - # year,month,day. - # compute fractional day from hour,minute,second,microsecond - fracday = hour / 24.0 + minute / 1440.0 + (second + microsecond/1.e6) / 86400.0 - jd = jd - 0.5 + fracday - - if isscalar: - return jd[0] - else: - return jd - -@cython.embedsignature(True) -def DateFromJulianDay(JD, calendar='standard', only_use_cftime_datetimes=True, - return_tuple=False): - """ - - returns a 'datetime-like' object given Julian Day. Julian Day is a - fractional day with approximately 100 microsecond accuracy. - - 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. - - If only_use_cftime_datetimes is set to True, then cftime.datetime - objects are returned for all calendars. Otherwise the datetime object is a - native python 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). - """ - - julian = np.atleast_1d(np.array(JD, dtype=np.longdouble)) - - def getdateinfo(julian): - # get the day (Z) and the fraction of the day (F) - # use 'round half up' rounding instead of numpy's even rounding - # so that 0.5 is rounded to 1.0, not 0 (cftime issue #49) - Z = np.atleast_1d(np.int32(_round_half_up(julian))) - F = (julian + 0.5 - Z).astype(np.longdouble) - - cdef Py_ssize_t i_max = len(Z) - year = np.empty(i_max, dtype=np.int32) - month = np.empty(i_max, dtype=np.int32) - day = np.empty(i_max, dtype=np.int32) - dayofyr = np.zeros(i_max,dtype=np.int32) - dayofwk = np.zeros(i_max,dtype=np.int32) - cdef int ijd - cdef Py_ssize_t i - for i in range(i_max): - ijd = Z[i] - year[i],month[i],day[i],dayofwk[i],dayofyr[i] = _IntJulianDayToDate(ijd,calendar) - - if calendar in ['standard', 'gregorian']: - ind_before = np.where(julian < 2299160.5) - ind_before = np.asarray(ind_before).any() - else: - ind_before = False - - # compute hour, minute, second, microsecond, convert to int32 - hour = np.clip((F * 24.).astype(np.int64), 0, 23) - F -= hour / 24. - minute = np.clip((F * 1440.).astype(np.int64), 0, 59) - second = np.clip((F - minute / 1440.) * 86400., 0, None) - microsecond = (second % 1)*1.e6 - hour = hour.astype(np.int32) - minute = minute.astype(np.int32) - second = second.astype(np.int32) - microsecond = microsecond.astype(np.int32) - - return year,month,day,hour,minute,second,microsecond,dayofyr,dayofwk,ind_before - - year,month,day,hour,minute,second,microsecond,dayofyr,dayofwk,ind_before =\ - getdateinfo(julian) - # round to nearest second if within ms_eps microseconds - # (to avoid ugly errors in datetime formatting - alternative - # to adding small offset all the time as was done previously) - # see netcdf4-python issue #433 and cftime issue #78 - # this is done by rounding microsends up or down, then - # recomputing year,month,day etc - # ms_eps is proportional to julian day, - # about 47 microseconds in 2000 for Julian base date in -4713 - ms_eps = np.atleast_1d(np.array(np.finfo(np.float64).eps,np.longdouble)) - ms_eps = 86400000000.*np.maximum(ms_eps*julian, ms_eps) - microsecond = np.where(microsecond < ms_eps, 0, microsecond) - indxms = microsecond > 1000000-ms_eps - if indxms.any(): - julian[indxms] = julian[indxms] + 2*ms_eps[indxms]/86400000000. - year[indxms],month[indxms],day[indxms],hour[indxms],minute[indxms],second[indxms],microsecond2,dayofyr[indxms],dayofwk[indxms],ind_before2 =\ - getdateinfo(julian[indxms]) - microsecond[indxms] = 0 - - # check if input was scalar and change return accordingly - isscalar = False - try: - JD[0] - except: - isscalar = True - - if calendar == 'proleptic_gregorian': - # datetime.datetime does not support years < 1 - #if year < 0: - if only_use_cftime_datetimes: - datetime_type = DatetimeProlepticGregorian - else: - if (year < 0).any(): # netcdftime issue #28 - datetime_type = DatetimeProlepticGregorian - else: - datetime_type = real_datetime - elif calendar in ('standard', 'gregorian'): - # return a 'real' datetime instance if calendar is proleptic - # Gregorian or Gregorian and all dates are after the - # Julian/Gregorian transition - if ind_before and not only_use_cftime_datetimes: - datetime_type = real_datetime - else: - datetime_type = DatetimeGregorian - elif calendar == "julian": - datetime_type = DatetimeJulian - elif calendar in ["noleap","365_day"]: - datetime_type = DatetimeNoLeap - elif calendar in ["all_leap","366_day"]: - datetime_type = DatetimeAllLeap - elif calendar == "360_day": - datetime_type = Datetime360Day - else: - raise ValueError("unsupported calendar: {0}".format(calendar)) - - if not isscalar: - if return_tuple: - return np.array([args for args in - zip(year, month, day, hour, minute, second, - microsecond,dayofwk,dayofyr)]) - else: - return np.array([datetime_type(*args) - for args in - zip(year, month, day, hour, minute, second, - microsecond)]) - - else: - if return_tuple: - return (year[0], month[0], day[0], hour[0], - minute[0], second[0], microsecond[0], - dayofwk[0], dayofyr[0]) - else: - return datetime_type(year[0], month[0], day[0], hour[0], - minute[0], second[0], microsecond[0]) - -class utime: - - """ -Performs conversions of netCDF time coordinate -data to/from datetime objects. - -To initialize: `t = utime(unit_string,calendar='standard'` - -where - -`unit_string` is a string of the form -`time-units since ` 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 `hours -since 0001-01-01 00:00:00`. months is allowed as a time unit -*only* for the 360_day calendar. - -The calendar keyword describes the calendar used in the time calculations. -All the values currently defined in the U{CF metadata convention -} -are accepted. The default is 'standard', which corresponds to the mixed -Gregorian/Julian calendar used by the udunits library. Valid calendars -are: - -'gregorian' or 'standard' (default): - -Mixed Gregorian/Julian calendar as defined by udunits. - -'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. - -'noleap' or '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. - -'360_day': - -All years are 360 days divided into 30 day months. - -'julian': - -Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a -leap year if it is divisible by 4. - -The num2date and date2num class methods can used to convert datetime -instances to/from the specified time units using the specified calendar. - -Example usage: - ->>> from cftime import utime ->>> from datetime import datetime ->>> cdftime = utime('hours since 0001-01-01 00:00:00') ->>> date = datetime.now() ->>> print date -2016-10-05 08:46:27.245015 ->>> ->>> t = cdftime.date2num(date) ->>> print t -17669840.7742 ->>> ->>> date = cdftime.num2date(t) ->>> print date -2016-10-05 08:46:27.244996 ->>> - -The resolution of the transformation operation is approximately a microsecond. - -Warning: Dates between 1582-10-5 and 1582-10-15 do not exist in the -'standard' or 'gregorian' calendars. An exception will be raised if you pass -a 'datetime-like' object in that range to the 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 `calendar` keyword). -@ivar unit_string: a string defining the the netCDF time variable. -@ivar units: the units part of `unit_string` (i.e. 'days', 'hours', 'seconds'). - """ - - def __init__(self, unit_string, calendar='standard', - only_use_cftime_datetimes=True,only_use_python_datetimes=False): - """ -@param unit_string: a string of the form -`time-units since ` 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 `hours -since 0001-01-01 00:00:00`. months is allowed as a time unit -*only* for the 360_day calendar. - -@keyword calendar: describes the calendar used in the time calculations. -All the values currently defined in the U{CF metadata convention -} -are accepted. The default is `standard`, which corresponds to the mixed -Gregorian/Julian calendar used by the udunits library. Valid calendars -are: - - `gregorian` or `standard` (default): - Mixed Gregorian/Julian calendar as defined by udunits. - - `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. - - `noleap` or `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. - -`360_day`: - All years are 360 days divided into 30 day months. - -`julian`: - Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a - leap year if it is divisible by 4. - -@keyword only_use_cftime_datetimes: if False, datetime.datetime -objects are returned from num2date where possible; if True dates which subclass -cftime.datetime are returned for all calendars. Default True. - -@keyword only_use_python_datetimes: always return python datetime.datetime -objects and raise an error if this is not possible. Ignored unless -**only_use_cftime_datetimes=False**. Default **False**. - -@returns: A class instance which may be used for converting times from netCDF -units to datetime objects. - """ - calendar = calendar.lower() - if calendar in _calendars: - self.calendar = calendar - else: - raise ValueError( - "calendar must be one of %s, got '%s'" % (str(_calendars), calendar)) - self.origin = _dateparse(unit_string,calendar=calendar) - units, isostring = _datesplit(unit_string) - self.units = units - self.unit_string = unit_string - self.only_use_cftime_datetimes = only_use_cftime_datetimes - self.only_use_python_datetimes = only_use_python_datetimes - - def date2num(self, date): - """ - Returns `time_value` in units described by `unit_string`, using - the specified `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 approximately a microsecond. - - If calendar = 'standard' or '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. - """ - return date2num(date,self.unit_string,calendar=self.calendar) - - def num2date(self, time_value): - """ - Return a 'datetime-like' object given a `time_value` in units - described by `unit_string`, using `calendar`. - - dates are in UTC with no offset, even if L{unit_string} contains - a time zone offset from UTC. - - Resolution is approximately a microsecond. - - Works for scalars, sequences and numpy arrays. - Returns a scalar if input is a scalar, else returns a numpy array. - """ - return num2date(time_value,self.unit_string,calendar=self.calendar,only_use_cftime_datetimes=self.only_use_cftime_datetimes,only_use_python_datetimes=self.only_use_python_datetimes) +# include legacy stuff no longer used by cftime.datetime +include "_cftime_legacy.pyx" diff --git a/src/cftime/_cftime_legacy.pyx b/src/cftime/_cftime_legacy.pyx new file mode 100644 index 00000000..5763e634 --- /dev/null +++ b/src/cftime/_cftime_legacy.pyx @@ -0,0 +1,633 @@ +# stuff below no longer used by cftime.datetime, kept here for backwards compatibility. + +@cython.embedsignature(True) +cdef class DatetimeNoLeap(datetime): + """ +Phony datetime object which mimics the python datetime object, +but uses the "noleap" ("365_day") calendar. + """ + def __init__(self, *args, **kwargs): + kwargs['calendar']='noleap' + super().__init__(*args, **kwargs) + def __repr__(self): + return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', + self.__class__.__name__, + self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) + cdef _getstate(self): + return (self.year, self.month, self.day, self.hour, + self.minute, self.second, self.microsecond, + self._dayofwk, self._dayofyr) + +@cython.embedsignature(True) +cdef class DatetimeAllLeap(datetime): + """ +Phony datetime object which mimics the python datetime object, +but uses the "all_leap" ("366_day") calendar. + """ + def __init__(self, *args, **kwargs): + kwargs['calendar']='all_leap' + super().__init__(*args, **kwargs) + def __repr__(self): + return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', + self.__class__.__name__, + self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) + cdef _getstate(self): + return (self.year, self.month, self.day, self.hour, + self.minute, self.second, self.microsecond, + self._dayofwk, self._dayofyr) + +@cython.embedsignature(True) +cdef class Datetime360Day(datetime): + """ +Phony datetime object which mimics the python datetime object, +but uses the "360_day" calendar. + """ + def __init__(self, *args, **kwargs): + kwargs['calendar']='360_day' + super().__init__(*args, **kwargs) + def __repr__(self): + return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', + self.__class__.__name__, + self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) + cdef _getstate(self): + return (self.year, self.month, self.day, self.hour, + self.minute, self.second, self.microsecond, + self._dayofwk, self._dayofyr) + +@cython.embedsignature(True) +cdef class DatetimeJulian(datetime): + """ +Phony datetime object which mimics the python datetime object, +but uses the "julian" calendar. + """ + def __init__(self, *args, **kwargs): + kwargs['calendar']='julian' + super().__init__(*args, **kwargs) + def __repr__(self): + return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', + self.__class__.__name__, + self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) + cdef _getstate(self): + return (self.year, self.month, self.day, self.hour, + self.minute, self.second, self.microsecond, + self._dayofwk, self._dayofyr) + +@cython.embedsignature(True) +cdef class DatetimeGregorian(datetime): + """ +Phony datetime object which mimics the python datetime object, +but uses the mixed Julian-Gregorian ("standard", "gregorian") calendar. + +The last date of the Julian calendar is 1582-10-4, which is followed +by 1582-10-15, using the Gregorian calendar. + +Instances using the date after 1582-10-15 can be compared to +datetime.datetime instances and used to compute time differences +(datetime.timedelta) by subtracting a DatetimeGregorian instance from +a datetime.datetime instance or vice versa. + """ + def __init__(self, *args, **kwargs): + kwargs['calendar']='gregorian' + super().__init__(*args, **kwargs) + def __repr__(self): + return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', + self.__class__.__name__, + self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) + cdef _getstate(self): + return (self.year, self.month, self.day, self.hour, + self.minute, self.second, self.microsecond, + self._dayofwk, self._dayofyr) + +@cython.embedsignature(True) +cdef class DatetimeProlepticGregorian(datetime): + """ +Phony datetime object which mimics the python datetime object, +but allows for dates that don't exist in the proleptic gregorian calendar. + +Supports timedelta operations by overloading + and -. + +Has strftime, timetuple, replace, __repr__, and __str__ methods. The +format of the string produced by __str__ is controlled by self.format +(default %Y-%m-%d %H:%M:%S). Supports comparisons with other +datetime instances using the same calendar; comparison with +native python datetime instances is possible for cftime.datetime +instances using 'gregorian' and 'proleptic_gregorian' calendars. + +Instance variables are year,month,day,hour,minute,second,microsecond,dayofwk,dayofyr, +format, and calendar. + """ + def __init__(self, *args, **kwargs): + kwargs['calendar']='proleptic_gregorian' + super().__init__( *args, **kwargs) + def __repr__(self): + return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime', + self.__class__.__name__, + self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond) + cdef _getstate(self): + return (self.year, self.month, self.day, self.hour, + self.minute, self.second, self.microsecond, + self._dayofwk, self._dayofyr) + + +# The following function (_IntJulianDayToDate) is based on +# algorithms described in the book +# "Calendrical Calculations" by Dershowitz and Rheingold, 3rd edition, Cambridge University Press, 2007 +# and the C implementation provided at https://reingold.co/calendar.C +# with modifications to handle non-real-world calendars and negative years. + +cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False,has_year_zero=False): + """Compute the year,month,day,dow,doy given the integer Julian day. + and calendar. (dow = day of week with 0=Mon,6=Sun and doy is day of year). + + Allowed calendars are 'standard', 'gregorian', 'julian', + 'proleptic_gregorian','360_day', '365_day', '366_day', 'noleap', + 'all_leap'. + + 'noleap' is a synonym for '365_day' + 'all_leap' is a synonym for '366_day' + 'gregorian' is a synonym for 'standard' + + optional kwarg 'skip_transition': When True, assume a 10-day + gap in Julian day numbers between Oct 4 and Oct 15 1582 (the transition + from Julian to Gregorian calendars). Default False, ignored + unless calendar = 'standard'.""" + cdef int year,month,day,dow,doy,yp1,jday_count,nextra + cdef int[12] dayspermonth + cdef int[13] cumdayspermonth + + # validate inputs. + calendar = _check_calendar(calendar) + + # compute day of week. + dow = (jday + 1) % 7 + # convert to ISO 8601 (0 = Monday, 6 = Sunday), like python datetime + dow -= 1 + if dow == -1: dow = 6 + + # handle all calendars except standard, julian, proleptic_gregorian. + if calendar == '360_day': + year = jday//360 + nextra = jday - year*360 + doy = nextra + 1 # Julday numbering starts at 0, doy starts at 1 + month = nextra//30 + 1 + day = doy - (month-1)*30 + return year,month,day,dow,doy + elif calendar == '365_day': + year = jday//365 + nextra = jday - year*365 + doy = nextra + 1 # Julday numbering starts at 0, doy starts at 1 + month = 1 + while doy > _cumdayspermonth[month]: + month += 1 + day = doy - _cumdayspermonth[month-1] + return year,month,day,dow,doy + elif calendar == '366_day': + year = jday//366 + nextra = jday - year*366 + doy = nextra + 1 # Julday numbering starts at 0, doy starts at 1 + month = 1 + while doy > _cumdayspermonth_leap[month]: + month += 1 + day = doy - _cumdayspermonth_leap[month-1] + return year,month,day,dow,doy + + # handle standard, julian, proleptic_gregorian calendars. + if jday < 0: + raise ValueError('julian day must be a positive integer') + + # start with initial guess of year that is before jday=1 in both + # Julian and Gregorian calendars. + year = jday//366 - 4714 + + # account for 10 days in Julian/Gregorian transition. + if not skip_transition and calendar == 'standard' and jday > 2299160: + jday += 10 + + yp1 = year + 1 + if yp1 == 0 and not has_year_zero: + yp1 = 1 # no year 0 + # initialize jday_count to Jan 1 of next year + jday_count = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero) + # Advance years until we find the right one + # (stop iteration when jday_count jday >= specified jday) + while jday >= jday_count: + year += 1 + if year == 0 and not has_year_zero: + year = 1 + yp1 = year + 1 + if yp1 == 0 and not has_year_zero: + yp1 = 1 + jday_count = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero) + # now we know year. + # set days in specified month, cumulative days in computed year. + if _is_leap(year, calendar,has_year_zero=has_year_zero): + dayspermonth = _dayspermonth_leap + cumdayspermonth = _cumdayspermonth_leap + else: + dayspermonth = _dayspermonth + cumdayspermonth = _cumdayspermonth + # initialized month to Jan, initialize jday_count to end of Jan of + # calculated year. + month = 1 + jday_count =\ + _IntJulianDayFromDate(year,month,dayspermonth[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero) + # now iterate by month until jday_count >= specified jday + while jday > jday_count: + month += 1 + jday_count =\ + _IntJulianDayFromDate(year,month,dayspermonth[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero) + # back up jday_count to 1st day of computed month + jday_count = _IntJulianDayFromDate(year,month,1,calendar,skip_transition=True,has_year_zero=has_year_zero) + # now jday_count represents day 1 of computed month in computed year + # so computed day is just difference between jday_count and specified jday. + day = jday - jday_count + 1 + # compute day in specified year. + doy = cumdayspermonth[month-1]+day + return year,month,day,dow,doy + +def _round_half_up(x): + # 'round half up' so 0.5 rounded to 1 (instead of 0 as in numpy.round) + return np.ceil(np.floor(2.*x)/2.) + +@cython.embedsignature(True) +def JulianDayFromDate(date, calendar='standard'): + """JulianDayFromDate(date, calendar='standard') + + creates a Julian Day from a 'datetime-like' object. Returns the fractional + Julian Day (approximately 100 microsecond accuracy). + + 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. + """ + + # check if input was scalar and change return accordingly + isscalar = False + try: + date[0] + except: + isscalar = True + + date = np.atleast_1d(np.array(date)) + year = np.empty(len(date), dtype=np.int32) + month = year.copy() + day = year.copy() + hour = year.copy() + minute = year.copy() + second = year.copy() + microsecond = year.copy() + jd = np.empty(year.shape, np.longdouble) + cdef long double[:] jd_view = jd + cdef Py_ssize_t i_max = len(date) + cdef Py_ssize_t i + for i in range(i_max): + d = date[i] + if getattr(d, 'tzinfo', None) is not None: + d = d.replace(tzinfo=None) - d.utcoffset() + + year[i] = d.year + month[i] = d.month + day[i] = d.day + hour[i] = d.hour + minute[i] = d.minute + second[i] = d.second + microsecond[i] = d.microsecond + jd_view[i] = _IntJulianDayFromDate(year[i],month[i],day[i],calendar) + + # at this point jd is an integer representing noon UTC on the given + # year,month,day. + # compute fractional day from hour,minute,second,microsecond + fracday = hour / 24.0 + minute / 1440.0 + (second + microsecond/1.e6) / 86400.0 + jd = jd - 0.5 + fracday + + if isscalar: + return jd[0] + else: + return jd + +@cython.embedsignature(True) +def DateFromJulianDay(JD, calendar='standard', only_use_cftime_datetimes=True, + return_tuple=False): + """ + + returns a 'datetime-like' object given Julian Day. Julian Day is a + fractional day with approximately 100 microsecond accuracy. + + 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. + + If only_use_cftime_datetimes is set to True, then cftime.datetime + objects are returned for all calendars. Otherwise the datetime object is a + native python 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). + """ + + julian = np.atleast_1d(np.array(JD, dtype=np.longdouble)) + + def getdateinfo(julian): + # get the day (Z) and the fraction of the day (F) + # use 'round half up' rounding instead of numpy's even rounding + # so that 0.5 is rounded to 1.0, not 0 (cftime issue #49) + Z = np.atleast_1d(np.int32(_round_half_up(julian))) + F = (julian + 0.5 - Z).astype(np.longdouble) + + cdef Py_ssize_t i_max = len(Z) + year = np.empty(i_max, dtype=np.int32) + month = np.empty(i_max, dtype=np.int32) + day = np.empty(i_max, dtype=np.int32) + dayofyr = np.zeros(i_max,dtype=np.int32) + dayofwk = np.zeros(i_max,dtype=np.int32) + cdef int ijd + cdef Py_ssize_t i + for i in range(i_max): + ijd = Z[i] + year[i],month[i],day[i],dayofwk[i],dayofyr[i] = _IntJulianDayToDate(ijd,calendar) + + if calendar in ['standard', 'gregorian']: + ind_before = np.where(julian < 2299160.5) + ind_before = np.asarray(ind_before).any() + else: + ind_before = False + + # compute hour, minute, second, microsecond, convert to int32 + hour = np.clip((F * 24.).astype(np.int64), 0, 23) + F -= hour / 24. + minute = np.clip((F * 1440.).astype(np.int64), 0, 59) + second = np.clip((F - minute / 1440.) * 86400., 0, None) + microsecond = (second % 1)*1.e6 + hour = hour.astype(np.int32) + minute = minute.astype(np.int32) + second = second.astype(np.int32) + microsecond = microsecond.astype(np.int32) + + return year,month,day,hour,minute,second,microsecond,dayofyr,dayofwk,ind_before + + year,month,day,hour,minute,second,microsecond,dayofyr,dayofwk,ind_before =\ + getdateinfo(julian) + # round to nearest second if within ms_eps microseconds + # (to avoid ugly errors in datetime formatting - alternative + # to adding small offset all the time as was done previously) + # see netcdf4-python issue #433 and cftime issue #78 + # this is done by rounding microsends up or down, then + # recomputing year,month,day etc + # ms_eps is proportional to julian day, + # about 47 microseconds in 2000 for Julian base date in -4713 + ms_eps = np.atleast_1d(np.array(np.finfo(np.float64).eps,np.longdouble)) + ms_eps = 86400000000.*np.maximum(ms_eps*julian, ms_eps) + microsecond = np.where(microsecond < ms_eps, 0, microsecond) + indxms = microsecond > 1000000-ms_eps + if indxms.any(): + julian[indxms] = julian[indxms] + 2*ms_eps[indxms]/86400000000. + year[indxms],month[indxms],day[indxms],hour[indxms],minute[indxms],second[indxms],microsecond2,dayofyr[indxms],dayofwk[indxms],ind_before2 =\ + getdateinfo(julian[indxms]) + microsecond[indxms] = 0 + + # check if input was scalar and change return accordingly + isscalar = False + try: + JD[0] + except: + isscalar = True + + if calendar == 'proleptic_gregorian': + # datetime.datetime does not support years < 1 + #if year < 0: + if only_use_cftime_datetimes: + datetime_type = DatetimeProlepticGregorian + else: + if (year < 0).any(): # netcdftime issue #28 + datetime_type = DatetimeProlepticGregorian + else: + datetime_type = real_datetime + elif calendar in ('standard', 'gregorian'): + # return a 'real' datetime instance if calendar is proleptic + # Gregorian or Gregorian and all dates are after the + # Julian/Gregorian transition + if ind_before and not only_use_cftime_datetimes: + datetime_type = real_datetime + else: + datetime_type = DatetimeGregorian + elif calendar == "julian": + datetime_type = DatetimeJulian + elif calendar in ["noleap","365_day"]: + datetime_type = DatetimeNoLeap + elif calendar in ["all_leap","366_day"]: + datetime_type = DatetimeAllLeap + elif calendar == "360_day": + datetime_type = Datetime360Day + else: + raise ValueError("unsupported calendar: {0}".format(calendar)) + + if not isscalar: + if return_tuple: + return np.array([args for args in + zip(year, month, day, hour, minute, second, + microsecond,dayofwk,dayofyr)]) + else: + return np.array([datetime_type(*args) + for args in + zip(year, month, day, hour, minute, second, + microsecond)]) + + else: + if return_tuple: + return (year[0], month[0], day[0], hour[0], + minute[0], second[0], microsecond[0], + dayofwk[0], dayofyr[0]) + else: + return datetime_type(year[0], month[0], day[0], hour[0], + minute[0], second[0], microsecond[0]) + +class utime: + + """ +Performs conversions of netCDF time coordinate +data to/from datetime objects. + +To initialize: `t = utime(unit_string,calendar='standard'` + +where + +`unit_string` is a string of the form +`time-units since ` 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 `hours +since 0001-01-01 00:00:00`. months is allowed as a time unit +*only* for the 360_day calendar. + +The calendar keyword describes the calendar used in the time calculations. +All the values currently defined in the U{CF metadata convention +} +are accepted. The default is 'standard', which corresponds to the mixed +Gregorian/Julian calendar used by the udunits library. Valid calendars +are: + +'gregorian' or 'standard' (default): + +Mixed Gregorian/Julian calendar as defined by udunits. + +'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. + +'noleap' or '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. + +'360_day': + +All years are 360 days divided into 30 day months. + +'julian': + +Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a +leap year if it is divisible by 4. + +The num2date and date2num class methods can used to convert datetime +instances to/from the specified time units using the specified calendar. + +Example usage: + +>>> from cftime import utime +>>> from datetime import datetime +>>> cdftime = utime('hours since 0001-01-01 00:00:00') +>>> date = datetime.now() +>>> print date +2016-10-05 08:46:27.245015 +>>> +>>> t = cdftime.date2num(date) +>>> print t +17669840.7742 +>>> +>>> date = cdftime.num2date(t) +>>> print date +2016-10-05 08:46:27.244996 +>>> + +The resolution of the transformation operation is approximately a microsecond. + +Warning: Dates between 1582-10-5 and 1582-10-15 do not exist in the +'standard' or 'gregorian' calendars. An exception will be raised if you pass +a 'datetime-like' object in that range to the 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 `calendar` keyword). +@ivar unit_string: a string defining the the netCDF time variable. +@ivar units: the units part of `unit_string` (i.e. 'days', 'hours', 'seconds'). + """ + + def __init__(self, unit_string, calendar='standard', + only_use_cftime_datetimes=True,only_use_python_datetimes=False): + """ +@param unit_string: a string of the form +`time-units since ` 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 `hours +since 0001-01-01 00:00:00`. months is allowed as a time unit +*only* for the 360_day calendar. + +@keyword calendar: describes the calendar used in the time calculations. +All the values currently defined in the U{CF metadata convention +} +are accepted. The default is `standard`, which corresponds to the mixed +Gregorian/Julian calendar used by the udunits library. Valid calendars +are: + - `gregorian` or `standard` (default): + Mixed Gregorian/Julian calendar as defined by udunits. + - `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. + - `noleap` or `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. + -`360_day`: + All years are 360 days divided into 30 day months. + -`julian`: + Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a + leap year if it is divisible by 4. + +@keyword only_use_cftime_datetimes: if False, datetime.datetime +objects are returned from num2date where possible; if True dates which subclass +cftime.datetime are returned for all calendars. Default True. + +@keyword only_use_python_datetimes: always return python datetime.datetime +objects and raise an error if this is not possible. Ignored unless +**only_use_cftime_datetimes=False**. Default **False**. + +@returns: A class instance which may be used for converting times from netCDF +units to datetime objects. + """ + calendar = calendar.lower() + if calendar in _calendars: + self.calendar = calendar + else: + raise ValueError( + "calendar must be one of %s, got '%s'" % (str(_calendars), calendar)) + self.origin = _dateparse(unit_string,calendar=calendar) + units, isostring = _datesplit(unit_string) + self.units = units + self.unit_string = unit_string + self.only_use_cftime_datetimes = only_use_cftime_datetimes + self.only_use_python_datetimes = only_use_python_datetimes + + def date2num(self, date): + """ + Returns `time_value` in units described by `unit_string`, using + the specified `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 approximately a microsecond. + + If calendar = 'standard' or '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. + """ + return date2num(date,self.unit_string,calendar=self.calendar) + + def num2date(self, time_value): + """ + Return a 'datetime-like' object given a `time_value` in units + described by `unit_string`, using `calendar`. + + dates are in UTC with no offset, even if L{unit_string} contains + a time zone offset from UTC. + + Resolution is approximately a microsecond. + + Works for scalars, sequences and numpy arrays. + Returns a scalar if input is a scalar, else returns a numpy array. + """ + return num2date(time_value,self.unit_string,calendar=self.calendar,only_use_cftime_datetimes=self.only_use_cftime_datetimes,only_use_python_datetimes=self.only_use_python_datetimes) diff --git a/test/test_cftime.py b/test/test_cftime.py index 751dd75e..0beed04f 100644 --- a/test/test_cftime.py +++ b/test/test_cftime.py @@ -290,10 +290,13 @@ def test_tz_naive(self): date = self.cdftime_jul.num2date(t) self.assertTrue(str(d) == str(date)) # test julian day from date, date from julian day - d = datetime(1858, 11, 17) - mjd = JulianDayFromDate(d) - assert_almost_equal(mjd, 2400000.5) - date = DateFromJulianDay(mjd) + d = cftime.datetime(1858, 11, 17, calendar='standard') + # toordinal should produce same result as JulidaDayFromDate + mjd1 = d.toordinal(fractional=True) + mjd2 = JulianDayFromDate(d) + assert_almost_equal(mjd1, 2400000.5) + assert_almost_equal(mjd1,mjd2) + date = DateFromJulianDay(mjd1) self.assertTrue(str(date) == str(d)) # test iso 8601 units string d = datetime(1970, 1, 1, 1) @@ -737,7 +740,8 @@ def roundtrip(delta,eps,units): test = dates == np.ma.masked_array([datetime(1848, 1, 17, 6, 0, 0, 40), None],mask=[0,1]) assert(test.all()) dates = num2date(times, units=units, calendar='standard') - assert(str(dates)=="[cftime.DatetimeGregorian(1848, 1, 17, 6, 0, 0, 40) --]") + assert(str(dates)==\ + "[cftime.datetime(1848, 1, 17, 6, 0, 0, 40, calendar='gregorian') --]") # check that time range of 200,000 + years can be represented accurately calendar='standard' _MAX_INT64 = np.iinfo("int64").max