From d908f127286eb6d214496095a2829824c1d15e99 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Fri, 29 Jan 2021 19:12:15 -0700 Subject: [PATCH 01/23] update --- Changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 6e1a8092..16e78b42 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` From a967811b0d17ef7db53f9f81f34d55af23a313fa Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Fri, 29 Jan 2021 20:56:07 -0700 Subject: [PATCH 02/23] update --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5871394a..da922b64 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ 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. + 1/17/2021: Version 1.3.1 released. 11/16/2020: Version 1.3.0 released. **API change**: The `cftime.datetime` constructor now creates From d58f693c612a10b50d4e411072f7c6c2264fa59e Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 08:32:32 -0700 Subject: [PATCH 03/23] add toordinal method, simplify dayofwk, dayofyr calculations. --- src/cftime/_cftime.pyx | 56 ++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index c7e92ba5..872b1c33 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -979,13 +979,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 +993,19 @@ 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': + cumdayspermonth = (self.month-1)*30 + dayofyr = cumdayspermonth+self.day + else: + if _is_leap(self.year,self.calendar,has_year_zero=self.has_year_zero): + cumdayspermonth = _cumdayspermonth_leap + else: + cumdayspermonth = _cumdayspermonth + if self.month == 1: + dayofyr = self.day + else: + dayofyr = cumdayspermonth[self.month-1]+self.day + # cache results for dayofyr self._dayofyr = dayofyr return dayofyr else: @@ -1147,6 +1154,19 @@ The default format of the string produced by strftime is controlled by self.form cdef _add_timedelta(self, other): return NotImplemented + def 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.""" + return _IntJulianDayFromDate(self.year, self.month, self.day, self.calendar, + skip_transition=False,has_year_zero=self.has_year_zero) + def __add__(self, other): cdef datetime dt if isinstance(self, datetime) and isinstance(other, timedelta): @@ -1156,6 +1176,7 @@ The default format of the string produced by strftime is controlled by self.form elif isinstance(self, timedelta) and isinstance(other, datetime): dt = other calendar = other.calendar + delta = self else: return NotImplemented @@ -1192,10 +1213,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 @@ -1252,6 +1271,7 @@ datetime object.""" return self - other._to_real_datetime() else: return NotImplemented + # these calendar-specific sub-classes are no longer used, but stubs # remain for backward compatibility. @@ -1748,6 +1768,8 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F else: return jday_greg +# stuff below no longer used, kept here for backwards compatibility. + 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). @@ -1861,8 +1883,6 @@ cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False,has_year_zero=F 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.) From 712c3efa6135f6c274c9cdd482afb4efd2c37e9b Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 08:33:55 -0700 Subject: [PATCH 04/23] update --- Changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog b/Changelog index 16e78b42..28ec0c1a 100644 --- a/Changelog +++ b/Changelog @@ -7,6 +7,7 @@ version 1.4.0 (release tag v1.4.0.rel) * 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). version 1.3.1 (release tag v1.3.1rel) ===================================== From 4437e0bf1bc353c649cc1533791bac340bd05127 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 08:39:37 -0700 Subject: [PATCH 05/23] update --- README.md | 3 ++- src/cftime/_cftime.pyx | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da922b64..edc72bed 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Time-handling functionality from netcdf4-python 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. +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. diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index 872b1c33..b2ef8b2e 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -1176,7 +1176,6 @@ The default format of the string produced by strftime is controlled by self.form elif isinstance(self, timedelta) and isinstance(other, datetime): dt = other calendar = other.calendar - delta = self else: return NotImplemented @@ -1271,7 +1270,6 @@ datetime object.""" return self - other._to_real_datetime() else: return NotImplemented - # these calendar-specific sub-classes are no longer used, but stubs # remain for backward compatibility. From 812522a22d2f729b5971a5dc13643cd2b5450d84 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 08:52:42 -0700 Subject: [PATCH 06/23] update --- docs/_build/html/_static/pygments.css | 8 ++++---- docs/_build/html/api.html | 11 +++++++++++ docs/_build/html/genindex.html | 2 ++ docs/_build/html/objects.inv | 5 ++++- docs/_build/html/searchindex.js | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) 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 From 3c729e49cd1b205f36b31ca1fc21a7d6f7be3589 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 11:03:05 -0700 Subject: [PATCH 07/23] update --- src/cftime/_cftime.pyx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index b2ef8b2e..29d63d09 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -1001,10 +1001,7 @@ The default format of the string produced by strftime is controlled by self.form cumdayspermonth = _cumdayspermonth_leap else: cumdayspermonth = _cumdayspermonth - if self.month == 1: - dayofyr = self.day - else: - dayofyr = cumdayspermonth[self.month-1]+self.day + dayofyr = cumdayspermonth[self.month-1]+self.day # cache results for dayofyr self._dayofyr = dayofyr return dayofyr @@ -1875,10 +1872,7 @@ cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False,has_year_zero=F # 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 + doy = cumdayspermonth[month-1]+day return year,month,day,dow,doy def _round_half_up(x): From 5be41ff2f62f7f7869eacb4819934410b2ea609e Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 14:02:09 -0700 Subject: [PATCH 08/23] remove redundant method for computing days in month --- src/cftime/_cftime.pyx | 42 +++++++++--------------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index 29d63d09..c5e91abf 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): @@ -1017,8 +990,11 @@ The default format of the string produced by strftime is controlled by self.form elif 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): """ From c1795f8572c4d55d92a67bb8e8c2df8c82b87812 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 14:20:27 -0700 Subject: [PATCH 09/23] update --- src/cftime/_cftime.pyx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index c5e91abf..09e1fb7f 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -983,11 +983,7 @@ 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: if _is_leap(self.year,self.calendar, From fddbf222d94ebff9525866e390176af398b5299c Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 15:15:04 -0700 Subject: [PATCH 10/23] update --- src/cftime/_cftime.pyx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index 09e1fb7f..d3e7c1a6 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -967,14 +967,12 @@ The default format of the string produced by strftime is controlled by self.form def dayofyr(self): if self._dayofyr < 0 and self.calendar: if self.calendar == '360_day': - cumdayspermonth = (self.month-1)*30 - dayofyr = cumdayspermonth+self.day + dayofyr = (self.month-1)*30+self.day else: if _is_leap(self.year,self.calendar,has_year_zero=self.has_year_zero): - cumdayspermonth = _cumdayspermonth_leap + dayofyr = _cumdayspermonth_leap[self.month-1]+self.day else: - cumdayspermonth = _cumdayspermonth - dayofyr = cumdayspermonth[self.month-1]+self.day + dayofyr = _cumdayspermonth[self.month-1]+self.day # cache results for dayofyr self._dayofyr = dayofyr return dayofyr From 68511756eb3b6e1cbc0f757141ff70ca0e204ca4 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 15:35:14 -0700 Subject: [PATCH 11/23] pin numpy --- .github/workflows/miniconda.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index 5b19c0a2..092991ae 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -40,6 +40,7 @@ jobs: run: | conda create --name TEST python=${{ matrix.python-version }} --file requirements.txt --file requirements-dev.txt source activate TEST + conda install numpy==1.19.4 # pin numpy version # enabling coverage slows down the tests dramaticaly #CYTHON_COVERAGE=1 pip install -v -e . --no-deps --force-reinstall pip install -v -e . --no-deps --force-reinstall From d2faabb4e57482d1d4d2535ea8439c3e80486744 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 15:42:47 -0700 Subject: [PATCH 12/23] update --- .github/workflows/miniconda.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index 092991ae..48da8d00 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -40,7 +40,8 @@ jobs: run: | conda create --name TEST python=${{ matrix.python-version }} --file requirements.txt --file requirements-dev.txt source activate TEST - conda install numpy==1.19.4 # pin numpy version + # install specfic version of numpy + conda install --name TEST -c conda-forge numpy==1.19.4 # enabling coverage slows down the tests dramaticaly #CYTHON_COVERAGE=1 pip install -v -e . --no-deps --force-reinstall pip install -v -e . --no-deps --force-reinstall From 900f485ed5eb36f9fc68f824c5e877abb7ecfa0d Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 15:50:05 -0700 Subject: [PATCH 13/23] update --- .github/workflows/miniconda.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index 48da8d00..b037a18a 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -40,10 +40,9 @@ jobs: run: | conda create --name TEST python=${{ matrix.python-version }} --file requirements.txt --file requirements-dev.txt source activate TEST - # install specfic version of numpy - conda install --name TEST -c conda-forge numpy==1.19.4 # enabling coverage slows down the tests dramaticaly #CYTHON_COVERAGE=1 pip install -v -e . --no-deps --force-reinstall + conda update --all pip install -v -e . --no-deps --force-reinstall conda info --all conda list From 8a52c6187ce5ec64b8d535e555445a0c2e5e120f Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 15:54:19 -0700 Subject: [PATCH 14/23] update --- .github/workflows/miniconda.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index b037a18a..5b19c0a2 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -42,7 +42,6 @@ jobs: source activate TEST # enabling coverage slows down the tests dramaticaly #CYTHON_COVERAGE=1 pip install -v -e . --no-deps --force-reinstall - conda update --all pip install -v -e . --no-deps --force-reinstall conda info --all conda list From f3dfd803d63a534e376b7a251aa6738f8f9a50fa Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 16:21:45 -0700 Subject: [PATCH 15/23] try not using conda-forge --- .github/workflows/miniconda.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index 5b19c0a2..45650d52 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -31,9 +31,9 @@ jobs: - name: Setup Conda uses: s-weigand/setup-conda@v1 - with: - activate-conda: false - conda-channels: conda-forge +# with: +# activate-conda: false +# conda-channels: conda-forge - name: Setup Conda Env shell: bash -l {0} From affa7e7274d700d4981d9ce674387bbf0a7d2d7e Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 16:22:09 -0700 Subject: [PATCH 16/23] update --- .github/workflows/miniconda.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index 45650d52..d966c289 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -31,8 +31,8 @@ jobs: - name: Setup Conda uses: s-weigand/setup-conda@v1 -# with: -# activate-conda: false + with: + activate-conda: false # conda-channels: conda-forge - name: Setup Conda Env From 23d77f542c2a2ab5b42b7a4059cb8e08425cbc86 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 16:31:54 -0700 Subject: [PATCH 17/23] update --- .github/workflows/miniconda.yml | 1 + requirements-dev.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index d966c289..04bcf636 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -68,6 +68,7 @@ jobs: shell: bash -l {0} run: | source activate TEST + conda install -c conda-forge check-manifest python setup.py --version ; pip wheel . -w dist --no-deps ; check-manifest --verbose ; diff --git a/requirements-dev.txt b/requirements-dev.txt index 92f83417..39909309 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ -check-manifest coverage coveralls cython From 695202026a883f7378f442f9f178f11eb5d9a783 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 30 Jan 2021 17:29:15 -0700 Subject: [PATCH 18/23] suppress numpy deprecation warning --- .github/workflows/miniconda.yml | 3 +-- requirements-dev.txt | 1 + setup.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/miniconda.yml b/.github/workflows/miniconda.yml index 04bcf636..5b19c0a2 100644 --- a/.github/workflows/miniconda.yml +++ b/.github/workflows/miniconda.yml @@ -33,7 +33,7 @@ jobs: uses: s-weigand/setup-conda@v1 with: activate-conda: false -# conda-channels: conda-forge + conda-channels: conda-forge - name: Setup Conda Env shell: bash -l {0} @@ -68,7 +68,6 @@ jobs: shell: bash -l {0} run: | source activate TEST - conda install -c conda-forge check-manifest python setup.py --version ; pip wheel . -w dist --no-deps ; check-manifest --verbose ; diff --git a/requirements-dev.txt b/requirements-dev.txt index 39909309..92f83417 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +check-manifest coverage coveralls cython 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) From 264acb7a084aa91da91c05b2015e8fe3115b47c5 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sun, 31 Jan 2021 11:40:56 -0700 Subject: [PATCH 19/23] don't use datetime subclasses internally in cftime.datetime --- src/cftime/_cftime.pyx | 339 ++++++++++++++---------------- test/test_cftime.py | 467 +++++++++++++++++++++-------------------- 2 files changed, 390 insertions(+), 416 deletions(-) diff --git a/src/cftime/_cftime.pyx b/src/cftime/_cftime.pyx index d3e7c1a6..a890be00 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -295,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 @@ -1112,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""" @@ -1121,7 +1089,7 @@ The default format of the string produced by strftime is controlled by self.form cdef _add_timedelta(self, other): return NotImplemented - def toordinal(self): + def toordinal(self,fractional=False): """Return julian day ordinal. January 1 of the year -4713 is day 0 for the julian,gregorian and standard @@ -1130,9 +1098,21 @@ The default format of the string produced by strftime is controlled by self.form 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.""" - return _IntJulianDayFromDate(self.year, self.month, self.day, self.calendar, + 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 @@ -1149,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 @@ -1202,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: @@ -1238,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)") @@ -1735,6 +1596,114 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F # stuff below no longer used, kept here for backwards compatibility. +# 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) + + 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). diff --git a/test/test_cftime.py b/test/test_cftime.py index 751dd75e..9b274327 100644 --- a/test/test_cftime.py +++ b/test/test_cftime.py @@ -14,10 +14,8 @@ import cftime from cftime import datetime as datetimex from cftime import real_datetime -from cftime import (DateFromJulianDay, Datetime360Day, DatetimeAllLeap, - DatetimeGregorian, DatetimeJulian, DatetimeNoLeap, - DatetimeProlepticGregorian, JulianDayFromDate, _parse_date, - date2index, date2num, num2date, utime, UNIT_CONVERSION_FACTORS) +from cftime import JulianDayFromDate, DateFromJulianDay, _parse_date,\ + date2index, date2num, num2date, utime, UNIT_CONVERSION_FACTORS try: from datetime import timezone @@ -290,10 +288,12 @@ 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') + 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) @@ -499,16 +499,16 @@ def roundtrip(delta,eps,units): units = "days since 0000-01-01 00:00:00" # this should fail (year zero not allowed with real-world calendars) try: - date2num(datetime(1, 1, 1), units, calendar='standard') + date2num(cftime.datetime(1, 1, 1), units, calendar='standard') except ValueError: pass # this should not fail (year zero allowed in 'fake' calendars) t = date2num(datetime(1, 1, 1), units, calendar='360_day') self.assertAlmostEqual(t,360) d = num2date(t, units, calendar='360_day') - self.assertEqual(d, Datetime360Day(1,1,1)) + self.assertEqual(d, cftime.datetime(1,1,1,calendar='360_day')) d = num2date(0, units, calendar='360_day') - self.assertEqual(d, Datetime360Day(0,1,1)) + self.assertEqual(d, cftime.datetime(0,1,1,calendar='360_day')) # issue 685: wrong time zone conversion # 'The following times all refer to the same moment: "18:30Z", "22:30+04", "1130-0700", and "15:00-03:30' @@ -561,7 +561,7 @@ def roundtrip(delta,eps,units): assert (date2.hour == date1.hour) assert (date2.minute == date1.minute) assert (date2.second == date1.second) - assert_almost_equal(JulianDayFromDate(date1), 1721057.5) + assert_almost_equal(date1.toordinal(fractional=True), 1721057.5) # issue 596 - negative years fail in utime.num2date u = utime("seconds since 1-1-1", "proleptic_gregorian") d = u.num2date(u.date2num(datetimex(-1, 1, 1))) @@ -628,29 +628,29 @@ def roundtrip(delta,eps,units): assert (d.minute == 0) assert (d.second == 0) # test dayofwk, dayofyr attribute setting (cftime issue #13) - d1 = DatetimeGregorian(2020,2,29) + d1 = cftime.datetime(2020,2,29,calendar='gregorian') d2 = real_datetime(2020,2,29) assert (d1.dayofwk == d2.dayofwk == 5) assert (d1.dayofyr == d2.dayofyr == 60) - d1 = DatetimeGregorian(2020,2,29,23,59,59) + d1 = cftime.datetime(2020,2,29,23,59,59,calendar='gregorian') d2 = real_datetime(2020,2,29,23,59,59) assert (d1.dayofwk == d2.dayofwk == 5) assert (d1.dayofyr == d2.dayofyr == 60) - d1 = DatetimeGregorian(2020,2,28,23,59,59) + d1 = cftime.datetime(2020,2,28,23,59,59,calendar='gregorian') d2 = real_datetime(2020,2,28,23,59,59) assert (d1.dayofwk == d2.dayofwk == 4) assert (d1.dayofyr == d2.dayofyr == 59) - d1 = DatetimeGregorian(1700,1,1) + d1 = cftime.datetime(1700,1,1,calendar='gregorian') d2 = real_datetime(1700,1,1) assert (d1.dayofwk == d2.dayofwk == 4) assert (d1.dayofyr == d2.dayofyr == 1) # last day of Julian Calendar (Thursday) - d1 = DatetimeJulian(1582, 10, 4, 12) - d2 = DatetimeGregorian(1582, 10, 4, 12) + d1 = cftime.datetime(1582, 10, 4, 12,calendar='julian') + d2 = cftime.datetime(1582, 10, 4, 12,calendar='standard') assert (d1.dayofwk == d2.dayofwk == 3) assert (d1.dayofyr == d2.dayofyr == 277) # Monday in proleptic gregorian calendar - d1 = DatetimeProlepticGregorian(1582, 10, 4, 12) + d1 = cftime.datetime(1582, 10, 4, 12,calendar='proleptic_gregorian') d2 = real_datetime(1582,10,4,12) assert (d1.dayofwk == d2.dayofwk == 0) assert (d1.dayofyr == d2.dayofyr == 277) @@ -679,7 +679,7 @@ def roundtrip(delta,eps,units): # issue #68: allow months since for 360_day calendar d = num2date(1, 'months since 0000-01-01 00:00:00', calendar='360_day') - self.assertEqual(d, Datetime360Day(0,2,1)) + self.assertEqual(d, cftime.datetime(0,2,1,calendar='360_day')) t = date2num(d, 'months since 0000-01-01 00:00:00', calendar='360_day') self.assertEqual(t, 1) # check that exception is raised if 'months since' used with @@ -691,9 +691,10 @@ def roundtrip(delta,eps,units): # issue #78 - extra digits due to roundoff assert(cftime.date2num(cftime.datetime(1, 12, 1, 0, 0, 0, 0, -1, 1), units='days since 01-01-01',calendar='noleap') == 334.0) assert(cftime.date2num(cftime.num2date(1.0,units='days since 01-01-01',calendar='noleap'),units='days since 01-01-01',calendar='noleap') == 1.0) - assert(cftime.date2num(cftime.DatetimeNoLeap(1980, 1, 1, 0, 0, 0, 0, 6, 1),'days since 1970-01-01','noleap') == 3650.0) + assert(cftime.date2num(cftime.datetime(1980, 1, 1, 0, 0, 0, 0, 6, + 1,calendar='noleap'),'days since 1970-01-01','noleap') == 3650.0) # issue #126 - d = cftime.DatetimeProlepticGregorian(1, 1, 1) + d = cftime.datetime(1, 1, 1,calendar='proleptic_gregorian') assert(cftime.date2num(d, 'days since 0001-01-01',\ 'proleptic_gregorian') == 0.0) # issue #140 (fractional seconds in reference date) @@ -737,11 +738,12 @@ 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 - refdate = DatetimeGregorian(292277,10,24,0,0,1) + refdate = cftime.datetime(292277,10,24,0,0,1,calendar='gregorian') for unit in ['microseconds','milliseconds','seconds']: units = '%s since 01-01-01' % unit time = 292471*365*86400*(1000000//int(UNIT_CONVERSION_FACTORS[unit])) + 1000000//int(UNIT_CONVERSION_FACTORS[unit]) @@ -755,7 +757,7 @@ def roundtrip(delta,eps,units): assert(date2 == refdate) # microsecond roundtrip accuracy preserved over time ranges of 286 years # (float64 can only represent integers exactly up to 2**53-1) - refdate=DatetimeGregorian(286,6,3,23,47,34,740992) + refdate=cftime.datetime(286,6,3,23,47,34,740992,calendar='gregorian') for unit in ['microseconds','milliseconds','seconds','hours','days']: units = '%s since 01-01-01' % unit time = (2**53 - 1)*(1/UNIT_CONVERSION_FACTORS[unit]) + 1/UNIT_CONVERSION_FACTORS[unit] @@ -808,7 +810,7 @@ def roundtrip(delta,eps,units): # (masked array handling in date2num - AttributeError: # 'cftime._cftime.DatetimeGregorian' object has no attribute 'view') m = np.ma.asarray( - [cftime.DatetimeGregorian(2014, 8, 1, 12, 0, 0, 0)] + [cftime.datetime(2014, 8, 1, 12, 0, 0, 0,calendar='gregorian')] ) assert( cftime.date2num(m, units="seconds since 2000-1-1")==[4.602096e+08] @@ -1020,9 +1022,9 @@ def setUp(self): def test_roundtrip(self): "Test roundtrip conversion (num2date <-> date2num) using 360_day and 365_day calendars." - for datetime_class in [Datetime360Day, DatetimeNoLeap]: + for cal in ['360_day','365_day']: # Pick a date and time outside of the range of the Julian calendar. - date = datetime_class(-5000, 1, 1, 12) + date = cftime.datetime(-5000, 1, 1, 12,calendar=cal) converter = self.converters[date.calendar] self.assertEqual(date, converter.num2date(converter.date2num(date))) @@ -1053,23 +1055,27 @@ def test_dayofwk(self): class DateTime(unittest.TestCase): def setUp(self): - self.date1_365_day = DatetimeNoLeap(-5000, 1, 2, 12) - self.date2_365_day = DatetimeNoLeap(-5000, 1, 3, 12) - self.date3_gregorian = DatetimeGregorian(1969, 7, 20, 12) + self.date1_365_day = cftime.datetime(-5000, 1, 2, 12,calendar='noleap') + self.date2_365_day = cftime.datetime(-5000, 1, 3, 12,calendar='noleap') + self.date3_gregorian = cftime.datetime(1969, 7, 20, + 12,calendar='gregorian') # last day of the Julian calendar in the mixed Julian/Gregorian calendar - self.date4_gregorian = DatetimeGregorian(1582, 10, 4) + self.date4_gregorian = cftime.datetime(1582, 10, + 4,calendar='gregorian') # first day of the Gregorian calendar in the mixed Julian/Gregorian calendar - self.date5_gregorian = DatetimeGregorian(1582, 10, 15) + self.date5_gregorian = cftime.datetime(1582, 10, + 15,calendar='gregorian') - self.date6_proleptic_gregorian = DatetimeProlepticGregorian(1582, 10, 15) + self.date6_proleptic_gregorian = cftime.datetime(1582, 10, + 15,calendar='proleptic_gregorian') - self.date7_360_day = Datetime360Day(2000, 1, 1) + self.date7_360_day = cftime.datetime(2000, 1, 1, calendar='360_day') - self.date8_julian = DatetimeJulian(1582, 10, 4) + self.date8_julian = cftime.datetime(1582, 10, 4,calendar='julian') # a datetime.datetime instance (proleptic Gregorian calendar) - self.datetime_date1 = datetime(1969, 7, 21, 12) + self.datetime_date1 = cftime.datetime(1969, 7, 21, 12) self.delta = timedelta(hours=25) @@ -1085,27 +1091,27 @@ def test_add(self): # test the Julian/Gregorian transition self.assertEqual(self.date4_gregorian + self.delta, - DatetimeGregorian(1582, 10, 15, 1)) + cftime.datetime(1582, 10, 15, 1,calendar='gregorian')) # The Julian calendar has no invalid dates self.assertEqual(self.date8_julian + self.delta, - DatetimeJulian(1582, 10, 5, 1)) + cftime.datetime(1582, 10, 5, 1,calendar='julian')) # Test going over the year boundary. - self.assertEqual(DatetimeGregorian(2000, 11, 1) + timedelta(days=30 + 31), - DatetimeGregorian(2001, 1, 1)) + self.assertEqual(cftime.datetime(2000, 11, 1,calendar='gregorian') + timedelta(days=30 + 31), + cftime.datetime(2001, 1, 1,calendar='gregorian')) # Year 2000 is a leap year. - self.assertEqual(DatetimeGregorian(2000, 1, 1) + timedelta(days=31 + 29), - DatetimeGregorian(2000, 3, 1)) + self.assertEqual(cftime.datetime(2000, 1, 1,calendar='gregorian') + timedelta(days=31 + 29), + cftime.datetime(2000, 3, 1,calendar='gregorian')) # Test the 366_day calendar. - self.assertEqual(DatetimeAllLeap(1, 1, 1) + timedelta(days=366 * 10 + 31), - DatetimeAllLeap(11, 2, 1)) + self.assertEqual(cftime.datetime(1, 1, 1,calendar='366_day') + timedelta(days=366 * 10 + 31), + cftime.datetime(11, 2, 1,calendar='366_day')) # The Gregorian calendar has no year zero. - self.assertEqual(DatetimeGregorian(-1, 12, 31) + self.delta, - DatetimeGregorian(1, 1, 1, 1)) + self.assertEqual(cftime.datetime(-1, 12, 31,calendar='gregorian') + self.delta, + cftime.datetime(1, 1, 1, 1,calendar='standard')) def invalid_add_1(): self.date1_365_day + 1 @@ -1144,28 +1150,30 @@ def total_seconds(td): # Test the Julian/Gregorian transition. self.assertEqual(self.date5_gregorian - self.delta, - DatetimeGregorian(1582, 10, 3, 23)) + cftime.datetime(1582, 10, 3, 23,calendar='gregorian')) # The proleptic Gregorian calendar does not have invalid dates. self.assertEqual(self.date6_proleptic_gregorian - self.delta, - DatetimeProlepticGregorian(1582, 10, 13, 23)) + cftime.datetime(1582, 10, 13, 23, + calendar='proleptic_gregorian')) # The Gregorian calendar has no year zero. - self.assertEqual(DatetimeGregorian(1, 1, 1) - self.delta, - DatetimeGregorian(-1, 12, 30, 23)) + self.assertEqual(cftime.datetime(1, 1, 1,calendar='gregorian') - self.delta, + cftime.datetime(-1, 12, 30, 23,calendar='gregorian')) # The 360_day calendar has year zero. self.assertEqual(self.date7_360_day - timedelta(days=2000 * 360), - Datetime360Day(0, 1, 1)) + cftime.datetime(0, 1, 1,calendar='360_day')) # Test going over the year boundary. - self.assertEqual(DatetimeGregorian(2000, 3, 1) - timedelta(days=29 + 31 + 31), - DatetimeGregorian(1999, 12, 1)) + self.assertEqual(cftime.datetime(2000, 3, 1,calendar='gregorian') -\ + timedelta(days=29 + 31 + 31),\ + cftime.datetime(1999, 12, 1,calendar='gregorian')) # Year 2000 is a leap year. - self.assertEqual(DatetimeGregorian(2000, 3, 1) - self.delta, - DatetimeGregorian(2000, 2, 28, 23)) + self.assertEqual(cftime.datetime(2000, 3, 1,calendar='gregorian') - self.delta, + cftime.datetime(2000, 2, 28, 23,calendar='gregorian')) def invalid_sub_1(): self.date1_365_day - 1 @@ -1205,7 +1213,8 @@ def test_pickling(self): "Test reversibility of pickling." import pickle - date = Datetime360Day(year=1, month=2, day=3, hour=4, minute=5, second=6, microsecond=7) + date = cftime.datetime(year=1, month=2, day=3, hour=4, minute=5, second=6, + microsecond=7,calendar='360_day') self.assertEqual(date, pickle.loads(pickle.dumps(date))) def test_misc(self): @@ -1218,16 +1227,16 @@ def test_misc(self): "1969-07-20 12:00:00") def invalid_year(): - DatetimeGregorian(0, 1, 1) + self.delta + cftime.datetime(0, 1, 1,calendar='gregorian') + self.delta def invalid_month(): - DatetimeGregorian(1, 13, 1) + self.delta + cftime.datetime(1, 13, 1,calendar='gregorian') + self.delta def invalid_day(): - DatetimeGregorian(1, 1, 32) + self.delta + cftime.datetime(1, 1, 32,calendar='gregorian') + self.delta def invalid_gregorian_date(): - DatetimeGregorian(1582, 10, 5) + self.delta + cftime.datetime(1582, 10, 5,calendar='gregorian') + self.delta for func in [invalid_year, invalid_month, invalid_day, invalid_gregorian_date]: self.assertRaises(ValueError, func) @@ -1385,12 +1394,8 @@ def test_parse_incorrect_unitstring(self): ValueError, cftime._cftime.date2num, datetime(1900, 1, 1, 0), datestr, 'standard') -_DATE_TYPES = [DatetimeNoLeap, DatetimeAllLeap, DatetimeJulian, Datetime360Day, - DatetimeGregorian, DatetimeProlepticGregorian] - - -@pytest.fixture(params=_DATE_TYPES) -def date_type(request): +@pytest.fixture(params=calendars) +def calendar(request): return request.param @@ -1400,69 +1405,70 @@ def month(request): @pytest.fixture -def days_per_month_non_leap_year(date_type, month): - if date_type is Datetime360Day: +def days_per_month_non_leap_year(calendar, month): + if calendar == '360_day': return [-1, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30][month] - if date_type is DatetimeAllLeap: + if calendar in ['all_leap','366_day']: return [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] else: return [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] @pytest.fixture -def days_per_month_leap_year(date_type, month): - if date_type is Datetime360Day: +def days_per_month_leap_year(calendar, month): + if calendar == '360_day': return [-1, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30][month] - if date_type in [DatetimeGregorian, DatetimeProlepticGregorian, - DatetimeJulian, DatetimeAllLeap]: + if calendar in ['julian','gregorian','proleptic_gregorian','standar','all_leap','366_day']: return [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] else: return [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] -def test_zero_year(date_type): +def test_zero_year(calendar): # Year 0 is valid in the 360,365 and 366 day calendars - if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day]: - date_type(0, 1, 1) + if calendar in ['no_leap','all_leap','360_day']: + cftime.datetime(0, 1, 1, calendar=calendar) else: with pytest.raises(ValueError): - date_type(0, 1, 1) + cftime.datetime(0, 1, 1,calendar=calendar) -def test_invalid_month(date_type): +def test_invalid_month(calendar): with pytest.raises(ValueError): - date_type(1, 0, 1) + cftime.datetime(1, 0, 1, calendar=calendar) with pytest.raises(ValueError): - date_type(1, 13, 1) + cftime.datetime(1, 13, 1, calendar=calendar) def test_invalid_day_non_leap_year( - date_type, month, days_per_month_non_leap_year): + calendar, month, days_per_month_non_leap_year): with pytest.raises(ValueError): - date_type(1, month, days_per_month_non_leap_year + 1) + cftime.datetime(1, month, days_per_month_non_leap_year+1, + calendar=calendar) -def test_invalid_day_leap_year(date_type, month, days_per_month_leap_year): +def test_invalid_day_leap_year(calendar, month, days_per_month_leap_year): with pytest.raises(ValueError): - date_type(2000, month, days_per_month_leap_year + 1) + cftime.datetime(2000, month, + days_per_month_leap_year+1,calendar=calendar) -def test_invalid_day_lower_bound(date_type, month): +def test_invalid_day_lower_bound(calendar, month): with pytest.raises(ValueError): - date_type(1, month, 0) + cftime.datetime(1, month, 0, calendar=calendar) def test_valid_day_non_leap_year( - date_type, month, days_per_month_non_leap_year): - date_type(1, month, 1) - date_type(1, month, days_per_month_non_leap_year) + calendar, month, days_per_month_non_leap_year): + cftime.datetime(1, month, 1, calendar=calendar) + cftime.datetime(1, month, days_per_month_non_leap_year, calendar=calendar) def test_valid_day_leap_year( - date_type, month, days_per_month_leap_year): - date_type(2000, month, 1) - date_type(2000, month, days_per_month_leap_year) + calendar, month, days_per_month_leap_year): + cftime.datetime(2000, month, 1, calendar=calendar) + cftime.datetime(2000, month, days_per_month_leap_year,calendar=calendar) _INVALID_SUB_DAY_TESTS = { @@ -1479,9 +1485,9 @@ def test_valid_day_leap_year( @pytest.mark.parametrize('date_args', list(_INVALID_SUB_DAY_TESTS.values()), ids=list(_INVALID_SUB_DAY_TESTS.keys())) -def test_invalid_sub_day_reso_dates(date_type, date_args): +def test_invalid_sub_day_reso_dates(calendar, date_args): with pytest.raises(ValueError): - date_type(*date_args) + cftime.datetime(*date_args,calendar=calendar) _VALID_SUB_DAY_TESTS = { @@ -1498,26 +1504,26 @@ def test_invalid_sub_day_reso_dates(date_type, date_args): @pytest.mark.parametrize('date_args', list(_VALID_SUB_DAY_TESTS.values()), ids=list(_VALID_SUB_DAY_TESTS.keys())) -def test_valid_sub_day_reso_dates(date_type, date_args): - date_type(*date_args) +def test_valid_sub_day_reso_dates(calendar, date_args): + cftime.datetime(*date_args,calendar=calendar) @pytest.mark.parametrize( 'date_args', [(1582, 10, 5), (1582, 10, 14)], ids=['lower-bound', 'upper-bound']) -def test_invalid_julian_gregorian_mixed_dates(date_type, date_args): - if date_type is DatetimeGregorian: +def test_invalid_julian_gregorian_mixed_dates(calendar, date_args): + if calendar in ['gregorian','standard']: with pytest.raises(ValueError): - date_type(*date_args) + cftime.datetime(*date_args,calendar=calendar) else: - date_type(*date_args) + cftime.datetime(*date_args,calendar=calendar) @pytest.mark.parametrize( 'date_args', [(1582, 10, 4), (1582, 10, 15)], ids=['lower-bound', 'upper-bound']) -def test_valid_julian_gregorian_mixed_dates(date_type, date_args): - date_type(*date_args) +def test_valid_julian_gregorian_mixed_dates(calendar, date_args): + cftime.datetime(*date_args,calendar=calendar) @pytest.mark.parametrize( @@ -1526,54 +1532,28 @@ def test_valid_julian_gregorian_mixed_dates(date_type, date_args): (1000, 2, 3, 4, 5, 6), (2000, 1, 1, 12, 34, 56, 123456)], ids=['1', '10', '100', '1000', '2000']) -def test_str_matches_datetime_str(date_type, date_args): - assert str(date_type(*date_args)) == str(datetime(*date_args)) +def test_str_matches_datetime_str(calendar, date_args): + assert str(cftime.datetime(*date_args),calendar=calendar) == str(datetime(*date_args)) -_EXPECTED_DATE_TYPES = {'noleap': DatetimeNoLeap, - '365_day': DatetimeNoLeap, - '360_day': Datetime360Day, - 'julian': DatetimeJulian, - 'all_leap': DatetimeAllLeap, - '366_day': DatetimeAllLeap, - 'gregorian': DatetimeGregorian, - 'proleptic_gregorian': DatetimeProlepticGregorian, - 'standard': DatetimeGregorian} - - -@pytest.mark.parametrize( - ['calendar', 'expected_date_type'], - list(_EXPECTED_DATE_TYPES.items()) -) -def test_num2date_only_use_cftime_datetimes_negative_years( - calendar, expected_date_type): +@pytest.mark.parametrize(calendars) +def test_num2date_only_use_cftime_datetimes_negative_years(calendar): result = num2date(-1000., units='days since 0001-01-01', calendar=calendar, only_use_cftime_datetimes=True) - assert isinstance(result, datetimex) assert (result.calendar == adjust_calendar(calendar)) -@pytest.mark.parametrize( - ['calendar', 'expected_date_type'], - list(_EXPECTED_DATE_TYPES.items()) -) -def test_num2date_only_use_cftime_datetimes_pre_gregorian( - calendar, expected_date_type): +@pytest.mark.parametrize(calendars) +def test_num2date_only_use_cftime_datetimes_pre_gregorian(calendar): result = num2date(1., units='days since 0001-01-01', calendar=calendar, only_use_cftime_datetimes=True) - assert isinstance(result, datetimex) assert (result.calendar == adjust_calendar(calendar)) -@pytest.mark.parametrize( - ['calendar', 'expected_date_type'], - list(_EXPECTED_DATE_TYPES.items()) -) -def test_num2date_only_use_cftime_datetimes_post_gregorian( - calendar, expected_date_type): +@pytest.mark.parametrize(calendars) +def test_num2date_only_use_cftime_datetimes_post_gregorian(calendar): result = num2date(0., units='days since 1582-10-15', calendar=calendar, only_use_cftime_datetimes=True) - assert isinstance(result, datetimex) assert (result.calendar == adjust_calendar(calendar)) @@ -1584,46 +1564,46 @@ def test_repr(): assert repr(datetimex(2000, 1, 1, calendar=None)) == expected -def test_dayofyr_after_replace(date_type): - date = date_type(1, 1, 1) +def test_dayofyr_after_replace(calendar): + date = cftime.datetime(1, 1, 1,calendar=calendar) assert date.dayofyr == 1 assert date.replace(day=2).dayofyr == 2 -def test_dayofwk_after_replace(date_type): - date = date_type(1, 1, 1) +def test_dayofwk_after_replace(calendar): + date = cftime.datetime(1, 1, 1,calendar=calendar) original_dayofwk = date.dayofwk expected = (original_dayofwk + 1) % 7 result = date.replace(day=2).dayofwk assert result == expected -def test_daysinmonth_non_leap(date_type, month, days_per_month_non_leap_year): - date = date_type(1, month, 1) +def test_daysinmonth_non_leap(calendar, month, days_per_month_non_leap_year): + date = cftime.datetime(1, month, 1,calendar=calendar) assert date.daysinmonth == days_per_month_non_leap_year -def test_daysinmonth_leap(date_type, month, days_per_month_leap_year): - date = date_type(2000, month, 1) +def test_daysinmonth_leap(calendar, month, days_per_month_leap_year): + date = cftime.datetime(2000, month, 1, calendar=calendar) assert date.daysinmonth == days_per_month_leap_year @pytest.mark.parametrize('argument', ['dayofyr', 'dayofwk']) -def test_replace_dayofyr_or_dayofwk_error(date_type, argument): +def test_replace_dayofyr_or_dayofwk_error(calendar, argument): with pytest.raises(ValueError): - date_type(1, 1, 1).replace(**{argument: 3}) + cftime.datetime(1, 1, 1,calendar=calendar).replace(**{argument: 3}) -def test_dayofyr_after_timedelta_addition(date_type): - initial_date = date_type(1, 1, 2) +def test_dayofyr_after_timedelta_addition(calendar): + initial_date = cftime.datetime(1, 1, 2,calendar=calendar) date_after_timedelta_addition = initial_date + timedelta(days=1) assert initial_date.dayofyr == 2 assert date_after_timedelta_addition.dayofyr == 3 -def test_exact_datetime_difference(date_type): - b = date_type(2000, 1, 2, 0, 0, 0, 5) - a = date_type(2000, 1, 2) +def test_exact_datetime_difference(calendar): + b = cftime.datetime(2000, 1, 2, 0, 0, 0, 5,calendar=calendar) + a = cftime.datetime(2000, 1, 2,calendar=calendar) result = b - a expected = timedelta(microseconds=5) assert result == expected @@ -1662,18 +1642,16 @@ def dtype(request): return request.param -@pytest.fixture(params=list(_EXPECTED_DATE_TYPES.keys())) -def calendar(request): - return request.param - - @pytest.mark.parametrize("unit", _MICROSECOND_UNITS) def test_num2date_microsecond_units(calendar, unit, shape, dtype): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 1, 0, 0, 0, 1), - date_type(2000, 1, 1, 0, 0, 0, 2), - date_type(2000, 1, 1, 0, 0, 0, 3), - date_type(2000, 1, 1, 0, 0, 0, 4)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 1, 1, 0, 0, 0, 1, + calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 0, 2, + calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 0, 3, + calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 0, 4, + calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1682,11 +1660,14 @@ def test_num2date_microsecond_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _MILLISECOND_UNITS) def test_num2date_millisecond_units(calendar, unit, shape, dtype): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 1, 0, 0, 0, 1000), - date_type(2000, 1, 1, 0, 0, 0, 2000), - date_type(2000, 1, 1, 0, 0, 0, 3000), - date_type(2000, 1, 1, 0, 0, 0, 4000)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 1, 1, 0, 0, 0, + 1000,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 0, + 2000,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 0, + 3000,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 0, + 4000,calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1695,11 +1676,14 @@ def test_num2date_millisecond_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _SECOND_UNITS) def test_num2date_second_units(calendar, unit, shape, dtype): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 1, 0, 0, 1, 0), - date_type(2000, 1, 1, 0, 0, 2, 0), - date_type(2000, 1, 1, 0, 0, 3, 0), - date_type(2000, 1, 1, 0, 0, 4, 0)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 1, 1, 0, 0, 1, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 2, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 3, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 0, 4, + 0,calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1708,11 +1692,14 @@ def test_num2date_second_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _MINUTE_UNITS) def test_num2date_minute_units(calendar, unit, shape, dtype): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 1, 0, 1, 0, 0), - date_type(2000, 1, 1, 0, 2, 0, 0), - date_type(2000, 1, 1, 0, 3, 0, 0), - date_type(2000, 1, 1, 0, 4, 0, 0)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 1, 1, 0, 1, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 2, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 3, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 0, 4, 0, + 0,calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1721,11 +1708,14 @@ def test_num2date_minute_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _HOUR_UNITS) def test_num2date_hour_units(calendar, unit, shape, dtype): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0), - date_type(2000, 1, 1, 2, 0, 0, 0), - date_type(2000, 1, 1, 3, 0, 0, 0), - date_type(2000, 1, 1, 4, 0, 0, 0)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 1, 1, 1, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 2, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 3, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 4, 0, 0, + 0,calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1734,11 +1724,14 @@ def test_num2date_hour_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _DAY_UNITS) def test_num2date_day_units(calendar, unit, shape, dtype): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0), - date_type(2000, 1, 3, 0, 0, 0, 0), - date_type(2000, 1, 4, 0, 0, 0, 0), - date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 1, 2, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 3, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 4, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 5, 0, 0, 0, + 0,calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1747,11 +1740,14 @@ def test_num2date_day_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _MONTH_UNITS) def test_num2date_month_units(calendar, unit, shape, dtype): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 2, 1, 0, 0, 0, 0), - date_type(2000, 3, 1, 0, 0, 0, 0), - date_type(2000, 4, 1, 0, 0, 0, 0), - date_type(2000, 5, 1, 0, 0, 0, 0)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 2, 1, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 3, 1, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 4, 1, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 5, 1, 0, 0, 0, + 0,calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) @@ -1764,11 +1760,14 @@ def test_num2date_month_units(calendar, unit, shape, dtype): def test_num2date_only_use_python_datetimes(calendar, shape, dtype): - date_type = real_datetime - expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0), - date_type(2000, 1, 3, 0, 0, 0, 0), - date_type(2000, 1, 4, 0, 0, 0, 0), - date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) + expected = np.array([cftime.datetime(2000, 1, 2, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 3, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 4, 0, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 5, 0, 0, 0, + 0,calendar=calendar)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "days since 2000-01-01" if calendar not in _STANDARD_CALENDARS: @@ -1783,22 +1782,22 @@ def test_num2date_only_use_python_datetimes(calendar, shape, dtype): np.testing.assert_equal(result, expected) -def test_num2date_use_pydatetime_if_possible(calendar, shape, dtype): - if calendar not in _STANDARD_CALENDARS: - date_type = _EXPECTED_DATE_TYPES[calendar] - else: - date_type = real_datetime - - expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0), - date_type(2000, 1, 3, 0, 0, 0, 0), - date_type(2000, 1, 4, 0, 0, 0, 0), - date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) - numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) - units = "days since 2000-01-01" - result = num2date(numeric_times, units=units, calendar=calendar, - only_use_python_datetimes=False, - only_use_cftime_datetimes=False) - np.testing.assert_equal(result, expected) +#def test_num2date_use_pydatetime_if_possible(calendar, shape, dtype): +# if calendar not in _STANDARD_CALENDARS: +# date_type = _EXPECTED_DATE_TYPES[calendar] +# else: +# date_type = real_datetime +# +# expected = np.array([cftime.datetime(2000, 1, 2, 0, 0, 0, 0), +# cftime.datetime(2000, 1, 3, 0, 0, 0, 0), +# cftime.datetime(2000, 1, 4, 0, 0, 0, 0), +# cftime.datetime(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) +# numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) +# units = "days since 2000-01-01" +# result = num2date(numeric_times, units=units, calendar=calendar, +# only_use_python_datetimes=False, +# only_use_cftime_datetimes=False) +# np.testing.assert_equal(result, expected) @pytest.mark.parametrize( @@ -1843,11 +1842,14 @@ def test_num2date_valid_zero_reference_year(artificial_calendar): def test_num2date_masked_array(calendar): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0), - date_type(2000, 1, 1, 2, 0, 0, 0), - date_type(2000, 1, 1, 3, 0, 0, 0), - date_type(2000, 1, 1, 4, 0, 0, 0)]) + expected = np.array([cftime.datetime(2000, 1, 1, 1, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 2, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 3, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 4, 0, 0, + 0,calendar=calendar)]) mask = [False, False, True, False] expected = np.ma.masked_array(expected, mask=mask) numeric_times = np.ma.masked_array([1, 2, 3, 4], mask=mask) @@ -1864,11 +1866,14 @@ def test_num2date_out_of_range(): def test_num2date_list_input(calendar): - date_type = _EXPECTED_DATE_TYPES[calendar] - expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0), - date_type(2000, 1, 1, 2, 0, 0, 0), - date_type(2000, 1, 1, 3, 0, 0, 0), - date_type(2000, 1, 1, 4, 0, 0, 0)]) + expected = np.array([cftime.datetime(2000, 1, 1, 1, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 2, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 3, 0, 0, + 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 4, 0, 0, + 0,calendar=calendar)]) numeric_times = [1, 2, 3, 4] units = "hours since 2000-01-01" result = num2date(numeric_times, units=units, calendar=calendar) @@ -1878,11 +1883,12 @@ def test_num2date_list_input(calendar): def test_num2date_integer_upcast_required(): numeric_times = np.array([30, 60, 90, 120], dtype=np.int32) units = "minutes since 2000-01-01" + calendar="360_day" expected = np.array([ - Datetime360Day(2000, 1, 1, 0, 30, 0), - Datetime360Day(2000, 1, 1, 1, 0, 0), - Datetime360Day(2000, 1, 1, 1, 30, 0), - Datetime360Day(2000, 1, 1, 2, 0, 0) + cftime.datetime(2000, 1, 1, 0, 30, 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 1, 0, 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 1, 30, 0,calendar=calendar), + cftime.datetime(2000, 1, 1, 2, 0, 0,calendar=calendar) ]) result = num2date(numeric_times, units=units, calendar="360_day") np.testing.assert_equal(result, expected) @@ -1905,13 +1911,12 @@ def test_num2date_integer_upcast_required(): ids=lambda x: f"{x!r}" ) def test_date2num_num2date_roundtrip(encoding_units, freq, calendar): - date_type = _EXPECTED_DATE_TYPES[calendar] lengthy_timedelta = timedelta(days=291000 * 360) times = np.array( [ - date_type(1, 1, 1), - date_type(1, 1, 1) + lengthy_timedelta, - date_type(1, 1, 1) + lengthy_timedelta + freq + cftime.datetime(1, 1, 1,calendar=calendar), + cftime.datetime(1, 1, 1,calendar=calendar) + lengthy_timedelta, + cftime.datetime(1, 1, 1,calendar=calendar) + lengthy_timedelta + freq ] ) units = f"{encoding_units} since 0001-01-01" From 5192cb526e3ba6e48fd2195895586bbb351e2b47 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sun, 31 Jan 2021 11:49:07 -0700 Subject: [PATCH 20/23] isolate legacy functions --- src/cftime/__init__.py | 10 +- src/cftime/_cftime.pyx | 610 +--------------------------------- src/cftime/_cftime_legacy.pyx | 609 +++++++++++++++++++++++++++++++++ 3 files changed, 618 insertions(+), 611 deletions(-) create mode 100644 src/cftime/_cftime_legacy.pyx diff --git a/src/cftime/__init__.py b/src/cftime/__init__.py index cbef82b2..f91cb5b1 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 microsec_units, millisec_units, \ - sec_units, hr_units, day_units, min_units + sec_units, hr_units, day_units, min_units,\ + UNIT_CONVERSION_FACTORS from ._cftime import num2date, date2num, date2index, num2pydate from ._cftime import __version__ +# legacy functions +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 a890be00..86af3921 100644 --- a/src/cftime/_cftime.pyx +++ b/src/cftime/_cftime.pyx @@ -1495,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 @@ -1594,609 +1594,5 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F else: return jday_greg -# stuff below no longer used, kept here for backwards compatibility. - -# 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) - - -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) +# 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..9374dda5 --- /dev/null +++ b/src/cftime/_cftime_legacy.pyx @@ -0,0 +1,609 @@ +# 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) + +@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) + + +# 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) From 479c318eef8d4dc680da862dc2e5193f58ab8900 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sun, 31 Jan 2021 12:15:13 -0700 Subject: [PATCH 21/23] update --- src/cftime/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cftime/__init__.py b/src/cftime/__init__.py index f91cb5b1..30e8e721 100644 --- a/src/cftime/__init__.py +++ b/src/cftime/__init__.py @@ -1,10 +1,10 @@ -from ._cftime import _parse_date, date2index, time2index, datetime, real_datetime +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,\ UNIT_CONVERSION_FACTORS -from ._cftime import num2date, date2num, date2index, num2pydate from ._cftime import __version__ -# legacy functions +# legacy functions in _cftime_legacy.pyx from ._cftime import DatetimeNoLeap, DatetimeAllLeap, Datetime360Day, DatetimeJulian, \ DatetimeGregorian, DatetimeProlepticGregorian from ._cftime import utime, JulianDayFromDate, DateFromJulianDay From b0f196c146cc9033f1faa3a3beff04fd3c194c6a Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sun, 31 Jan 2021 13:25:24 -0700 Subject: [PATCH 22/23] update --- src/cftime/_cftime_legacy.pyx | 40 ++- test/test_cftime.py | 454 +++++++++++++++++----------------- 2 files changed, 258 insertions(+), 236 deletions(-) diff --git a/src/cftime/_cftime_legacy.pyx b/src/cftime/_cftime_legacy.pyx index 9374dda5..5763e634 100644 --- a/src/cftime/_cftime_legacy.pyx +++ b/src/cftime/_cftime_legacy.pyx @@ -11,8 +11,12 @@ but uses the "noleap" ("365_day") calendar. 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) + 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): @@ -25,8 +29,12 @@ but uses the "all_leap" ("366_day") calendar. 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) + 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): @@ -39,8 +47,12 @@ but uses the "360_day" calendar. 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) + 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): @@ -53,8 +65,12 @@ but uses the "julian" calendar. 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) + 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): @@ -77,6 +93,10 @@ a datetime.datetime instance or vice versa. 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): @@ -103,6 +123,10 @@ format, and calendar. 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 diff --git a/test/test_cftime.py b/test/test_cftime.py index 9b274327..c35053ee 100644 --- a/test/test_cftime.py +++ b/test/test_cftime.py @@ -14,8 +14,10 @@ import cftime from cftime import datetime as datetimex from cftime import real_datetime -from cftime import JulianDayFromDate, DateFromJulianDay, _parse_date,\ - date2index, date2num, num2date, utime, UNIT_CONVERSION_FACTORS +from cftime import (DateFromJulianDay, Datetime360Day, DatetimeAllLeap, + DatetimeGregorian, DatetimeJulian, DatetimeNoLeap, + DatetimeProlepticGregorian, JulianDayFromDate, _parse_date, + date2index, date2num, num2date, utime, UNIT_CONVERSION_FACTORS) try: from datetime import timezone @@ -499,16 +501,16 @@ def roundtrip(delta,eps,units): units = "days since 0000-01-01 00:00:00" # this should fail (year zero not allowed with real-world calendars) try: - date2num(cftime.datetime(1, 1, 1), units, calendar='standard') + date2num(datetime(1, 1, 1), units, calendar='standard') except ValueError: pass # this should not fail (year zero allowed in 'fake' calendars) t = date2num(datetime(1, 1, 1), units, calendar='360_day') self.assertAlmostEqual(t,360) d = num2date(t, units, calendar='360_day') - self.assertEqual(d, cftime.datetime(1,1,1,calendar='360_day')) + self.assertEqual(d, Datetime360Day(1,1,1)) d = num2date(0, units, calendar='360_day') - self.assertEqual(d, cftime.datetime(0,1,1,calendar='360_day')) + self.assertEqual(d, Datetime360Day(0,1,1)) # issue 685: wrong time zone conversion # 'The following times all refer to the same moment: "18:30Z", "22:30+04", "1130-0700", and "15:00-03:30' @@ -561,7 +563,7 @@ def roundtrip(delta,eps,units): assert (date2.hour == date1.hour) assert (date2.minute == date1.minute) assert (date2.second == date1.second) - assert_almost_equal(date1.toordinal(fractional=True), 1721057.5) + assert_almost_equal(JulianDayFromDate(date1), 1721057.5) # issue 596 - negative years fail in utime.num2date u = utime("seconds since 1-1-1", "proleptic_gregorian") d = u.num2date(u.date2num(datetimex(-1, 1, 1))) @@ -628,29 +630,29 @@ def roundtrip(delta,eps,units): assert (d.minute == 0) assert (d.second == 0) # test dayofwk, dayofyr attribute setting (cftime issue #13) - d1 = cftime.datetime(2020,2,29,calendar='gregorian') + d1 = DatetimeGregorian(2020,2,29) d2 = real_datetime(2020,2,29) assert (d1.dayofwk == d2.dayofwk == 5) assert (d1.dayofyr == d2.dayofyr == 60) - d1 = cftime.datetime(2020,2,29,23,59,59,calendar='gregorian') + d1 = DatetimeGregorian(2020,2,29,23,59,59) d2 = real_datetime(2020,2,29,23,59,59) assert (d1.dayofwk == d2.dayofwk == 5) assert (d1.dayofyr == d2.dayofyr == 60) - d1 = cftime.datetime(2020,2,28,23,59,59,calendar='gregorian') + d1 = DatetimeGregorian(2020,2,28,23,59,59) d2 = real_datetime(2020,2,28,23,59,59) assert (d1.dayofwk == d2.dayofwk == 4) assert (d1.dayofyr == d2.dayofyr == 59) - d1 = cftime.datetime(1700,1,1,calendar='gregorian') + d1 = DatetimeGregorian(1700,1,1) d2 = real_datetime(1700,1,1) assert (d1.dayofwk == d2.dayofwk == 4) assert (d1.dayofyr == d2.dayofyr == 1) # last day of Julian Calendar (Thursday) - d1 = cftime.datetime(1582, 10, 4, 12,calendar='julian') - d2 = cftime.datetime(1582, 10, 4, 12,calendar='standard') + d1 = DatetimeJulian(1582, 10, 4, 12) + d2 = DatetimeGregorian(1582, 10, 4, 12) assert (d1.dayofwk == d2.dayofwk == 3) assert (d1.dayofyr == d2.dayofyr == 277) # Monday in proleptic gregorian calendar - d1 = cftime.datetime(1582, 10, 4, 12,calendar='proleptic_gregorian') + d1 = DatetimeProlepticGregorian(1582, 10, 4, 12) d2 = real_datetime(1582,10,4,12) assert (d1.dayofwk == d2.dayofwk == 0) assert (d1.dayofyr == d2.dayofyr == 277) @@ -679,7 +681,7 @@ def roundtrip(delta,eps,units): # issue #68: allow months since for 360_day calendar d = num2date(1, 'months since 0000-01-01 00:00:00', calendar='360_day') - self.assertEqual(d, cftime.datetime(0,2,1,calendar='360_day')) + self.assertEqual(d, Datetime360Day(0,2,1)) t = date2num(d, 'months since 0000-01-01 00:00:00', calendar='360_day') self.assertEqual(t, 1) # check that exception is raised if 'months since' used with @@ -691,10 +693,9 @@ def roundtrip(delta,eps,units): # issue #78 - extra digits due to roundoff assert(cftime.date2num(cftime.datetime(1, 12, 1, 0, 0, 0, 0, -1, 1), units='days since 01-01-01',calendar='noleap') == 334.0) assert(cftime.date2num(cftime.num2date(1.0,units='days since 01-01-01',calendar='noleap'),units='days since 01-01-01',calendar='noleap') == 1.0) - assert(cftime.date2num(cftime.datetime(1980, 1, 1, 0, 0, 0, 0, 6, - 1,calendar='noleap'),'days since 1970-01-01','noleap') == 3650.0) + assert(cftime.date2num(cftime.DatetimeNoLeap(1980, 1, 1, 0, 0, 0, 0, 6, 1),'days since 1970-01-01','noleap') == 3650.0) # issue #126 - d = cftime.datetime(1, 1, 1,calendar='proleptic_gregorian') + d = cftime.DatetimeProlepticGregorian(1, 1, 1) assert(cftime.date2num(d, 'days since 0001-01-01',\ 'proleptic_gregorian') == 0.0) # issue #140 (fractional seconds in reference date) @@ -743,7 +744,7 @@ def roundtrip(delta,eps,units): # check that time range of 200,000 + years can be represented accurately calendar='standard' _MAX_INT64 = np.iinfo("int64").max - refdate = cftime.datetime(292277,10,24,0,0,1,calendar='gregorian') + refdate = DatetimeGregorian(292277,10,24,0,0,1) for unit in ['microseconds','milliseconds','seconds']: units = '%s since 01-01-01' % unit time = 292471*365*86400*(1000000//int(UNIT_CONVERSION_FACTORS[unit])) + 1000000//int(UNIT_CONVERSION_FACTORS[unit]) @@ -757,7 +758,7 @@ def roundtrip(delta,eps,units): assert(date2 == refdate) # microsecond roundtrip accuracy preserved over time ranges of 286 years # (float64 can only represent integers exactly up to 2**53-1) - refdate=cftime.datetime(286,6,3,23,47,34,740992,calendar='gregorian') + refdate=DatetimeGregorian(286,6,3,23,47,34,740992) for unit in ['microseconds','milliseconds','seconds','hours','days']: units = '%s since 01-01-01' % unit time = (2**53 - 1)*(1/UNIT_CONVERSION_FACTORS[unit]) + 1/UNIT_CONVERSION_FACTORS[unit] @@ -810,7 +811,7 @@ def roundtrip(delta,eps,units): # (masked array handling in date2num - AttributeError: # 'cftime._cftime.DatetimeGregorian' object has no attribute 'view') m = np.ma.asarray( - [cftime.datetime(2014, 8, 1, 12, 0, 0, 0,calendar='gregorian')] + [cftime.DatetimeGregorian(2014, 8, 1, 12, 0, 0, 0)] ) assert( cftime.date2num(m, units="seconds since 2000-1-1")==[4.602096e+08] @@ -1022,9 +1023,9 @@ def setUp(self): def test_roundtrip(self): "Test roundtrip conversion (num2date <-> date2num) using 360_day and 365_day calendars." - for cal in ['360_day','365_day']: + for datetime_class in [Datetime360Day, DatetimeNoLeap]: # Pick a date and time outside of the range of the Julian calendar. - date = cftime.datetime(-5000, 1, 1, 12,calendar=cal) + date = datetime_class(-5000, 1, 1, 12) converter = self.converters[date.calendar] self.assertEqual(date, converter.num2date(converter.date2num(date))) @@ -1055,27 +1056,23 @@ def test_dayofwk(self): class DateTime(unittest.TestCase): def setUp(self): - self.date1_365_day = cftime.datetime(-5000, 1, 2, 12,calendar='noleap') - self.date2_365_day = cftime.datetime(-5000, 1, 3, 12,calendar='noleap') - self.date3_gregorian = cftime.datetime(1969, 7, 20, - 12,calendar='gregorian') + self.date1_365_day = DatetimeNoLeap(-5000, 1, 2, 12) + self.date2_365_day = DatetimeNoLeap(-5000, 1, 3, 12) + self.date3_gregorian = DatetimeGregorian(1969, 7, 20, 12) # last day of the Julian calendar in the mixed Julian/Gregorian calendar - self.date4_gregorian = cftime.datetime(1582, 10, - 4,calendar='gregorian') + self.date4_gregorian = DatetimeGregorian(1582, 10, 4) # first day of the Gregorian calendar in the mixed Julian/Gregorian calendar - self.date5_gregorian = cftime.datetime(1582, 10, - 15,calendar='gregorian') + self.date5_gregorian = DatetimeGregorian(1582, 10, 15) - self.date6_proleptic_gregorian = cftime.datetime(1582, 10, - 15,calendar='proleptic_gregorian') + self.date6_proleptic_gregorian = DatetimeProlepticGregorian(1582, 10, 15) - self.date7_360_day = cftime.datetime(2000, 1, 1, calendar='360_day') + self.date7_360_day = Datetime360Day(2000, 1, 1) - self.date8_julian = cftime.datetime(1582, 10, 4,calendar='julian') + self.date8_julian = DatetimeJulian(1582, 10, 4) # a datetime.datetime instance (proleptic Gregorian calendar) - self.datetime_date1 = cftime.datetime(1969, 7, 21, 12) + self.datetime_date1 = datetime(1969, 7, 21, 12) self.delta = timedelta(hours=25) @@ -1091,27 +1088,27 @@ def test_add(self): # test the Julian/Gregorian transition self.assertEqual(self.date4_gregorian + self.delta, - cftime.datetime(1582, 10, 15, 1,calendar='gregorian')) + DatetimeGregorian(1582, 10, 15, 1)) # The Julian calendar has no invalid dates self.assertEqual(self.date8_julian + self.delta, - cftime.datetime(1582, 10, 5, 1,calendar='julian')) + DatetimeJulian(1582, 10, 5, 1)) # Test going over the year boundary. - self.assertEqual(cftime.datetime(2000, 11, 1,calendar='gregorian') + timedelta(days=30 + 31), - cftime.datetime(2001, 1, 1,calendar='gregorian')) + self.assertEqual(DatetimeGregorian(2000, 11, 1) + timedelta(days=30 + 31), + DatetimeGregorian(2001, 1, 1)) # Year 2000 is a leap year. - self.assertEqual(cftime.datetime(2000, 1, 1,calendar='gregorian') + timedelta(days=31 + 29), - cftime.datetime(2000, 3, 1,calendar='gregorian')) + self.assertEqual(DatetimeGregorian(2000, 1, 1) + timedelta(days=31 + 29), + DatetimeGregorian(2000, 3, 1)) # Test the 366_day calendar. - self.assertEqual(cftime.datetime(1, 1, 1,calendar='366_day') + timedelta(days=366 * 10 + 31), - cftime.datetime(11, 2, 1,calendar='366_day')) + self.assertEqual(DatetimeAllLeap(1, 1, 1) + timedelta(days=366 * 10 + 31), + DatetimeAllLeap(11, 2, 1)) # The Gregorian calendar has no year zero. - self.assertEqual(cftime.datetime(-1, 12, 31,calendar='gregorian') + self.delta, - cftime.datetime(1, 1, 1, 1,calendar='standard')) + self.assertEqual(DatetimeGregorian(-1, 12, 31) + self.delta, + DatetimeGregorian(1, 1, 1, 1)) def invalid_add_1(): self.date1_365_day + 1 @@ -1150,30 +1147,28 @@ def total_seconds(td): # Test the Julian/Gregorian transition. self.assertEqual(self.date5_gregorian - self.delta, - cftime.datetime(1582, 10, 3, 23,calendar='gregorian')) + DatetimeGregorian(1582, 10, 3, 23)) # The proleptic Gregorian calendar does not have invalid dates. self.assertEqual(self.date6_proleptic_gregorian - self.delta, - cftime.datetime(1582, 10, 13, 23, - calendar='proleptic_gregorian')) + DatetimeProlepticGregorian(1582, 10, 13, 23)) # The Gregorian calendar has no year zero. - self.assertEqual(cftime.datetime(1, 1, 1,calendar='gregorian') - self.delta, - cftime.datetime(-1, 12, 30, 23,calendar='gregorian')) + self.assertEqual(DatetimeGregorian(1, 1, 1) - self.delta, + DatetimeGregorian(-1, 12, 30, 23)) # The 360_day calendar has year zero. self.assertEqual(self.date7_360_day - timedelta(days=2000 * 360), - cftime.datetime(0, 1, 1,calendar='360_day')) + Datetime360Day(0, 1, 1)) # Test going over the year boundary. - self.assertEqual(cftime.datetime(2000, 3, 1,calendar='gregorian') -\ - timedelta(days=29 + 31 + 31),\ - cftime.datetime(1999, 12, 1,calendar='gregorian')) + self.assertEqual(DatetimeGregorian(2000, 3, 1) - timedelta(days=29 + 31 + 31), + DatetimeGregorian(1999, 12, 1)) # Year 2000 is a leap year. - self.assertEqual(cftime.datetime(2000, 3, 1,calendar='gregorian') - self.delta, - cftime.datetime(2000, 2, 28, 23,calendar='gregorian')) + self.assertEqual(DatetimeGregorian(2000, 3, 1) - self.delta, + DatetimeGregorian(2000, 2, 28, 23)) def invalid_sub_1(): self.date1_365_day - 1 @@ -1213,8 +1208,7 @@ def test_pickling(self): "Test reversibility of pickling." import pickle - date = cftime.datetime(year=1, month=2, day=3, hour=4, minute=5, second=6, - microsecond=7,calendar='360_day') + date = Datetime360Day(year=1, month=2, day=3, hour=4, minute=5, second=6, microsecond=7) self.assertEqual(date, pickle.loads(pickle.dumps(date))) def test_misc(self): @@ -1227,16 +1221,16 @@ def test_misc(self): "1969-07-20 12:00:00") def invalid_year(): - cftime.datetime(0, 1, 1,calendar='gregorian') + self.delta + DatetimeGregorian(0, 1, 1) + self.delta def invalid_month(): - cftime.datetime(1, 13, 1,calendar='gregorian') + self.delta + DatetimeGregorian(1, 13, 1) + self.delta def invalid_day(): - cftime.datetime(1, 1, 32,calendar='gregorian') + self.delta + DatetimeGregorian(1, 1, 32) + self.delta def invalid_gregorian_date(): - cftime.datetime(1582, 10, 5,calendar='gregorian') + self.delta + DatetimeGregorian(1582, 10, 5) + self.delta for func in [invalid_year, invalid_month, invalid_day, invalid_gregorian_date]: self.assertRaises(ValueError, func) @@ -1394,8 +1388,12 @@ def test_parse_incorrect_unitstring(self): ValueError, cftime._cftime.date2num, datetime(1900, 1, 1, 0), datestr, 'standard') -@pytest.fixture(params=calendars) -def calendar(request): +_DATE_TYPES = [DatetimeNoLeap, DatetimeAllLeap, DatetimeJulian, Datetime360Day, + DatetimeGregorian, DatetimeProlepticGregorian] + + +@pytest.fixture(params=_DATE_TYPES) +def date_type(request): return request.param @@ -1405,70 +1403,69 @@ def month(request): @pytest.fixture -def days_per_month_non_leap_year(calendar, month): - if calendar == '360_day': +def days_per_month_non_leap_year(date_type, month): + if date_type is Datetime360Day: return [-1, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30][month] - if calendar in ['all_leap','366_day']: + if date_type is DatetimeAllLeap: return [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] else: return [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] @pytest.fixture -def days_per_month_leap_year(calendar, month): - if calendar == '360_day': +def days_per_month_leap_year(date_type, month): + if date_type is Datetime360Day: return [-1, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30][month] - if calendar in ['julian','gregorian','proleptic_gregorian','standar','all_leap','366_day']: + if date_type in [DatetimeGregorian, DatetimeProlepticGregorian, + DatetimeJulian, DatetimeAllLeap]: return [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] else: return [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] -def test_zero_year(calendar): +def test_zero_year(date_type): # Year 0 is valid in the 360,365 and 366 day calendars - if calendar in ['no_leap','all_leap','360_day']: - cftime.datetime(0, 1, 1, calendar=calendar) + if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day]: + date_type(0, 1, 1) else: with pytest.raises(ValueError): - cftime.datetime(0, 1, 1,calendar=calendar) + date_type(0, 1, 1) -def test_invalid_month(calendar): +def test_invalid_month(date_type): with pytest.raises(ValueError): - cftime.datetime(1, 0, 1, calendar=calendar) + date_type(1, 0, 1) with pytest.raises(ValueError): - cftime.datetime(1, 13, 1, calendar=calendar) + date_type(1, 13, 1) def test_invalid_day_non_leap_year( - calendar, month, days_per_month_non_leap_year): + date_type, month, days_per_month_non_leap_year): with pytest.raises(ValueError): - cftime.datetime(1, month, days_per_month_non_leap_year+1, - calendar=calendar) + date_type(1, month, days_per_month_non_leap_year + 1) -def test_invalid_day_leap_year(calendar, month, days_per_month_leap_year): +def test_invalid_day_leap_year(date_type, month, days_per_month_leap_year): with pytest.raises(ValueError): - cftime.datetime(2000, month, - days_per_month_leap_year+1,calendar=calendar) + date_type(2000, month, days_per_month_leap_year + 1) -def test_invalid_day_lower_bound(calendar, month): +def test_invalid_day_lower_bound(date_type, month): with pytest.raises(ValueError): - cftime.datetime(1, month, 0, calendar=calendar) + date_type(1, month, 0) def test_valid_day_non_leap_year( - calendar, month, days_per_month_non_leap_year): - cftime.datetime(1, month, 1, calendar=calendar) - cftime.datetime(1, month, days_per_month_non_leap_year, calendar=calendar) + date_type, month, days_per_month_non_leap_year): + date_type(1, month, 1) + date_type(1, month, days_per_month_non_leap_year) def test_valid_day_leap_year( - calendar, month, days_per_month_leap_year): - cftime.datetime(2000, month, 1, calendar=calendar) - cftime.datetime(2000, month, days_per_month_leap_year,calendar=calendar) + date_type, month, days_per_month_leap_year): + date_type(2000, month, 1) + date_type(2000, month, days_per_month_leap_year) _INVALID_SUB_DAY_TESTS = { @@ -1485,9 +1482,9 @@ def test_valid_day_leap_year( @pytest.mark.parametrize('date_args', list(_INVALID_SUB_DAY_TESTS.values()), ids=list(_INVALID_SUB_DAY_TESTS.keys())) -def test_invalid_sub_day_reso_dates(calendar, date_args): +def test_invalid_sub_day_reso_dates(date_type, date_args): with pytest.raises(ValueError): - cftime.datetime(*date_args,calendar=calendar) + date_type(*date_args) _VALID_SUB_DAY_TESTS = { @@ -1504,26 +1501,26 @@ def test_invalid_sub_day_reso_dates(calendar, date_args): @pytest.mark.parametrize('date_args', list(_VALID_SUB_DAY_TESTS.values()), ids=list(_VALID_SUB_DAY_TESTS.keys())) -def test_valid_sub_day_reso_dates(calendar, date_args): - cftime.datetime(*date_args,calendar=calendar) +def test_valid_sub_day_reso_dates(date_type, date_args): + date_type(*date_args) @pytest.mark.parametrize( 'date_args', [(1582, 10, 5), (1582, 10, 14)], ids=['lower-bound', 'upper-bound']) -def test_invalid_julian_gregorian_mixed_dates(calendar, date_args): - if calendar in ['gregorian','standard']: +def test_invalid_julian_gregorian_mixed_dates(date_type, date_args): + if date_type is DatetimeGregorian: with pytest.raises(ValueError): - cftime.datetime(*date_args,calendar=calendar) + date_type(*date_args) else: - cftime.datetime(*date_args,calendar=calendar) + date_type(*date_args) @pytest.mark.parametrize( 'date_args', [(1582, 10, 4), (1582, 10, 15)], ids=['lower-bound', 'upper-bound']) -def test_valid_julian_gregorian_mixed_dates(calendar, date_args): - cftime.datetime(*date_args,calendar=calendar) +def test_valid_julian_gregorian_mixed_dates(date_type, date_args): + date_type(*date_args) @pytest.mark.parametrize( @@ -1532,28 +1529,54 @@ def test_valid_julian_gregorian_mixed_dates(calendar, date_args): (1000, 2, 3, 4, 5, 6), (2000, 1, 1, 12, 34, 56, 123456)], ids=['1', '10', '100', '1000', '2000']) -def test_str_matches_datetime_str(calendar, date_args): - assert str(cftime.datetime(*date_args),calendar=calendar) == str(datetime(*date_args)) +def test_str_matches_datetime_str(date_type, date_args): + assert str(date_type(*date_args)) == str(datetime(*date_args)) -@pytest.mark.parametrize(calendars) -def test_num2date_only_use_cftime_datetimes_negative_years(calendar): +_EXPECTED_DATE_TYPES = {'noleap': DatetimeNoLeap, + '365_day': DatetimeNoLeap, + '360_day': Datetime360Day, + 'julian': DatetimeJulian, + 'all_leap': DatetimeAllLeap, + '366_day': DatetimeAllLeap, + 'gregorian': DatetimeGregorian, + 'proleptic_gregorian': DatetimeProlepticGregorian, + 'standard': DatetimeGregorian} + + +@pytest.mark.parametrize( + ['calendar', 'expected_date_type'], + list(_EXPECTED_DATE_TYPES.items()) +) +def test_num2date_only_use_cftime_datetimes_negative_years( + calendar, expected_date_type): result = num2date(-1000., units='days since 0001-01-01', calendar=calendar, only_use_cftime_datetimes=True) + assert isinstance(result, datetimex) assert (result.calendar == adjust_calendar(calendar)) -@pytest.mark.parametrize(calendars) -def test_num2date_only_use_cftime_datetimes_pre_gregorian(calendar): +@pytest.mark.parametrize( + ['calendar', 'expected_date_type'], + list(_EXPECTED_DATE_TYPES.items()) +) +def test_num2date_only_use_cftime_datetimes_pre_gregorian( + calendar, expected_date_type): result = num2date(1., units='days since 0001-01-01', calendar=calendar, only_use_cftime_datetimes=True) + assert isinstance(result, datetimex) assert (result.calendar == adjust_calendar(calendar)) -@pytest.mark.parametrize(calendars) -def test_num2date_only_use_cftime_datetimes_post_gregorian(calendar): +@pytest.mark.parametrize( + ['calendar', 'expected_date_type'], + list(_EXPECTED_DATE_TYPES.items()) +) +def test_num2date_only_use_cftime_datetimes_post_gregorian( + calendar, expected_date_type): result = num2date(0., units='days since 1582-10-15', calendar=calendar, only_use_cftime_datetimes=True) + assert isinstance(result, datetimex) assert (result.calendar == adjust_calendar(calendar)) @@ -1564,46 +1587,46 @@ def test_repr(): assert repr(datetimex(2000, 1, 1, calendar=None)) == expected -def test_dayofyr_after_replace(calendar): - date = cftime.datetime(1, 1, 1,calendar=calendar) +def test_dayofyr_after_replace(date_type): + date = date_type(1, 1, 1) assert date.dayofyr == 1 assert date.replace(day=2).dayofyr == 2 -def test_dayofwk_after_replace(calendar): - date = cftime.datetime(1, 1, 1,calendar=calendar) +def test_dayofwk_after_replace(date_type): + date = date_type(1, 1, 1) original_dayofwk = date.dayofwk expected = (original_dayofwk + 1) % 7 result = date.replace(day=2).dayofwk assert result == expected -def test_daysinmonth_non_leap(calendar, month, days_per_month_non_leap_year): - date = cftime.datetime(1, month, 1,calendar=calendar) +def test_daysinmonth_non_leap(date_type, month, days_per_month_non_leap_year): + date = date_type(1, month, 1) assert date.daysinmonth == days_per_month_non_leap_year -def test_daysinmonth_leap(calendar, month, days_per_month_leap_year): - date = cftime.datetime(2000, month, 1, calendar=calendar) +def test_daysinmonth_leap(date_type, month, days_per_month_leap_year): + date = date_type(2000, month, 1) assert date.daysinmonth == days_per_month_leap_year @pytest.mark.parametrize('argument', ['dayofyr', 'dayofwk']) -def test_replace_dayofyr_or_dayofwk_error(calendar, argument): +def test_replace_dayofyr_or_dayofwk_error(date_type, argument): with pytest.raises(ValueError): - cftime.datetime(1, 1, 1,calendar=calendar).replace(**{argument: 3}) + date_type(1, 1, 1).replace(**{argument: 3}) -def test_dayofyr_after_timedelta_addition(calendar): - initial_date = cftime.datetime(1, 1, 2,calendar=calendar) +def test_dayofyr_after_timedelta_addition(date_type): + initial_date = date_type(1, 1, 2) date_after_timedelta_addition = initial_date + timedelta(days=1) assert initial_date.dayofyr == 2 assert date_after_timedelta_addition.dayofyr == 3 -def test_exact_datetime_difference(calendar): - b = cftime.datetime(2000, 1, 2, 0, 0, 0, 5,calendar=calendar) - a = cftime.datetime(2000, 1, 2,calendar=calendar) +def test_exact_datetime_difference(date_type): + b = date_type(2000, 1, 2, 0, 0, 0, 5) + a = date_type(2000, 1, 2) result = b - a expected = timedelta(microseconds=5) assert result == expected @@ -1642,16 +1665,18 @@ def dtype(request): return request.param +@pytest.fixture(params=list(_EXPECTED_DATE_TYPES.keys())) +def calendar(request): + return request.param + + @pytest.mark.parametrize("unit", _MICROSECOND_UNITS) def test_num2date_microsecond_units(calendar, unit, shape, dtype): - expected = np.array([cftime.datetime(2000, 1, 1, 0, 0, 0, 1, - calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 0, 2, - calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 0, 3, - calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 0, 4, - calendar=calendar)]).reshape(shape) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 1, 0, 0, 0, 1), + date_type(2000, 1, 1, 0, 0, 0, 2), + date_type(2000, 1, 1, 0, 0, 0, 3), + date_type(2000, 1, 1, 0, 0, 0, 4)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1660,14 +1685,11 @@ def test_num2date_microsecond_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _MILLISECOND_UNITS) def test_num2date_millisecond_units(calendar, unit, shape, dtype): - expected = np.array([cftime.datetime(2000, 1, 1, 0, 0, 0, - 1000,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 0, - 2000,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 0, - 3000,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 0, - 4000,calendar=calendar)]).reshape(shape) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 1, 0, 0, 0, 1000), + date_type(2000, 1, 1, 0, 0, 0, 2000), + date_type(2000, 1, 1, 0, 0, 0, 3000), + date_type(2000, 1, 1, 0, 0, 0, 4000)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1676,14 +1698,11 @@ def test_num2date_millisecond_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _SECOND_UNITS) def test_num2date_second_units(calendar, unit, shape, dtype): - expected = np.array([cftime.datetime(2000, 1, 1, 0, 0, 1, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 2, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 3, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 0, 4, - 0,calendar=calendar)]).reshape(shape) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 1, 0, 0, 1, 0), + date_type(2000, 1, 1, 0, 0, 2, 0), + date_type(2000, 1, 1, 0, 0, 3, 0), + date_type(2000, 1, 1, 0, 0, 4, 0)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1692,14 +1711,11 @@ def test_num2date_second_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _MINUTE_UNITS) def test_num2date_minute_units(calendar, unit, shape, dtype): - expected = np.array([cftime.datetime(2000, 1, 1, 0, 1, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 2, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 3, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 0, 4, 0, - 0,calendar=calendar)]).reshape(shape) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 1, 0, 1, 0, 0), + date_type(2000, 1, 1, 0, 2, 0, 0), + date_type(2000, 1, 1, 0, 3, 0, 0), + date_type(2000, 1, 1, 0, 4, 0, 0)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1708,14 +1724,11 @@ def test_num2date_minute_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _HOUR_UNITS) def test_num2date_hour_units(calendar, unit, shape, dtype): - expected = np.array([cftime.datetime(2000, 1, 1, 1, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 2, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 3, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 4, 0, 0, - 0,calendar=calendar)]).reshape(shape) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0), + date_type(2000, 1, 1, 2, 0, 0, 0), + date_type(2000, 1, 1, 3, 0, 0, 0), + date_type(2000, 1, 1, 4, 0, 0, 0)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1724,14 +1737,11 @@ def test_num2date_hour_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _DAY_UNITS) def test_num2date_day_units(calendar, unit, shape, dtype): - expected = np.array([cftime.datetime(2000, 1, 2, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 3, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 4, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 5, 0, 0, 0, - 0,calendar=calendar)]).reshape(shape) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0), + date_type(2000, 1, 3, 0, 0, 0, 0), + date_type(2000, 1, 4, 0, 0, 0, 0), + date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) result = num2date(numeric_times, units=units, calendar=calendar) @@ -1740,14 +1750,11 @@ def test_num2date_day_units(calendar, unit, shape, dtype): @pytest.mark.parametrize("unit", _MONTH_UNITS) def test_num2date_month_units(calendar, unit, shape, dtype): - expected = np.array([cftime.datetime(2000, 2, 1, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 3, 1, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 4, 1, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 5, 1, 0, 0, 0, - 0,calendar=calendar)]).reshape(shape) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 2, 1, 0, 0, 0, 0), + date_type(2000, 3, 1, 0, 0, 0, 0), + date_type(2000, 4, 1, 0, 0, 0, 0), + date_type(2000, 5, 1, 0, 0, 0, 0)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "{} since 2000-01-01".format(unit) @@ -1760,14 +1767,11 @@ def test_num2date_month_units(calendar, unit, shape, dtype): def test_num2date_only_use_python_datetimes(calendar, shape, dtype): - expected = np.array([cftime.datetime(2000, 1, 2, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 3, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 4, 0, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 5, 0, 0, 0, - 0,calendar=calendar)]).reshape(shape) + date_type = real_datetime + expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0), + date_type(2000, 1, 3, 0, 0, 0, 0), + date_type(2000, 1, 4, 0, 0, 0, 0), + date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) units = "days since 2000-01-01" if calendar not in _STANDARD_CALENDARS: @@ -1782,22 +1786,22 @@ def test_num2date_only_use_python_datetimes(calendar, shape, dtype): np.testing.assert_equal(result, expected) -#def test_num2date_use_pydatetime_if_possible(calendar, shape, dtype): -# if calendar not in _STANDARD_CALENDARS: -# date_type = _EXPECTED_DATE_TYPES[calendar] -# else: -# date_type = real_datetime -# -# expected = np.array([cftime.datetime(2000, 1, 2, 0, 0, 0, 0), -# cftime.datetime(2000, 1, 3, 0, 0, 0, 0), -# cftime.datetime(2000, 1, 4, 0, 0, 0, 0), -# cftime.datetime(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) -# numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) -# units = "days since 2000-01-01" -# result = num2date(numeric_times, units=units, calendar=calendar, -# only_use_python_datetimes=False, -# only_use_cftime_datetimes=False) -# np.testing.assert_equal(result, expected) +def test_num2date_use_pydatetime_if_possible(calendar, shape, dtype): + if calendar not in _STANDARD_CALENDARS: + date_type = _EXPECTED_DATE_TYPES[calendar] + else: + date_type = real_datetime + + expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0), + date_type(2000, 1, 3, 0, 0, 0, 0), + date_type(2000, 1, 4, 0, 0, 0, 0), + date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape) + numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype) + units = "days since 2000-01-01" + result = num2date(numeric_times, units=units, calendar=calendar, + only_use_python_datetimes=False, + only_use_cftime_datetimes=False) + np.testing.assert_equal(result, expected) @pytest.mark.parametrize( @@ -1842,14 +1846,11 @@ def test_num2date_valid_zero_reference_year(artificial_calendar): def test_num2date_masked_array(calendar): - expected = np.array([cftime.datetime(2000, 1, 1, 1, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 2, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 3, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 4, 0, 0, - 0,calendar=calendar)]) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0), + date_type(2000, 1, 1, 2, 0, 0, 0), + date_type(2000, 1, 1, 3, 0, 0, 0), + date_type(2000, 1, 1, 4, 0, 0, 0)]) mask = [False, False, True, False] expected = np.ma.masked_array(expected, mask=mask) numeric_times = np.ma.masked_array([1, 2, 3, 4], mask=mask) @@ -1866,14 +1867,11 @@ def test_num2date_out_of_range(): def test_num2date_list_input(calendar): - expected = np.array([cftime.datetime(2000, 1, 1, 1, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 2, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 3, 0, 0, - 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 4, 0, 0, - 0,calendar=calendar)]) + date_type = _EXPECTED_DATE_TYPES[calendar] + expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0), + date_type(2000, 1, 1, 2, 0, 0, 0), + date_type(2000, 1, 1, 3, 0, 0, 0), + date_type(2000, 1, 1, 4, 0, 0, 0)]) numeric_times = [1, 2, 3, 4] units = "hours since 2000-01-01" result = num2date(numeric_times, units=units, calendar=calendar) @@ -1883,12 +1881,11 @@ def test_num2date_list_input(calendar): def test_num2date_integer_upcast_required(): numeric_times = np.array([30, 60, 90, 120], dtype=np.int32) units = "minutes since 2000-01-01" - calendar="360_day" expected = np.array([ - cftime.datetime(2000, 1, 1, 0, 30, 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 1, 0, 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 1, 30, 0,calendar=calendar), - cftime.datetime(2000, 1, 1, 2, 0, 0,calendar=calendar) + Datetime360Day(2000, 1, 1, 0, 30, 0), + Datetime360Day(2000, 1, 1, 1, 0, 0), + Datetime360Day(2000, 1, 1, 1, 30, 0), + Datetime360Day(2000, 1, 1, 2, 0, 0) ]) result = num2date(numeric_times, units=units, calendar="360_day") np.testing.assert_equal(result, expected) @@ -1911,12 +1908,13 @@ def test_num2date_integer_upcast_required(): ids=lambda x: f"{x!r}" ) def test_date2num_num2date_roundtrip(encoding_units, freq, calendar): + date_type = _EXPECTED_DATE_TYPES[calendar] lengthy_timedelta = timedelta(days=291000 * 360) times = np.array( [ - cftime.datetime(1, 1, 1,calendar=calendar), - cftime.datetime(1, 1, 1,calendar=calendar) + lengthy_timedelta, - cftime.datetime(1, 1, 1,calendar=calendar) + lengthy_timedelta + freq + date_type(1, 1, 1), + date_type(1, 1, 1) + lengthy_timedelta, + date_type(1, 1, 1) + lengthy_timedelta + freq ] ) units = f"{encoding_units} since 0001-01-01" From 603f47ae9e025fff357ebdcd45a1b44d391641da Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sun, 31 Jan 2021 13:27:34 -0700 Subject: [PATCH 23/23] update --- Changelog | 4 +++- test/test_cftime.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 28ec0c1a..f0f5fda6 100644 --- a/Changelog +++ b/Changelog @@ -7,7 +7,9 @@ version 1.4.0 (release tag v1.4.0.rel) * 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). + * 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/test/test_cftime.py b/test/test_cftime.py index c35053ee..0beed04f 100644 --- a/test/test_cftime.py +++ b/test/test_cftime.py @@ -291,6 +291,7 @@ def test_tz_naive(self): self.assertTrue(str(d) == str(date)) # test julian day from date, date from julian day 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)