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$dwl&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[}&hfSJ^DgOQ 2;!{``eEw9R߳!3<}1A fIM'~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)