Introduction

Languages like Python and SAS use different methods and offsets for calendrical functions.   Additional confusion often arises as a result of manipulating date and datetime values originating in the different time zones around the world.   Different Python modules define a variety of dates and times objects and functions.   In this section we start with Python’s built-in datetime module.   The topics covered are:

•     dates

•     times

•     datetimes

•     timedelta

•     tzinfo (time zone info)

Date

The Python date object represents a value for dates composed of calendar year,   month,   and day based on the current Gregorian calendar.   January 1st,   year 1 is day 1,   January 2nd of year 1 is day 2,   and so on.   Python date values range from January 1,   0001 to December 31,  9999.   Internally the date object is composed of values for year,   month,   and day.

SAS uses three counters for date,   datetime,   and time values.   SAS date constants use the number of days from the offset of January 1,   1960 with the range of valid SAS date values of January 1,   1582 to December 31,   20,000.   SAS datetime constants use the number of seconds from the offset of midnight,   January 1,   1960.   SAS time constants begin at midnight and increment to 86,400,   the number of seconds in a day.   SAS time constants have a resolution of one second.

Return today's dates calling the today function.

from datetime import date
now = date.today()
print('\n', 'Today is:     ', now,
      '\n', 'now Data Type:', type(now))
 Today is:      2019-01-06
 now Data Type: <class 'datetime.date'>


SAS analog.

data _null_;
   now = today();
   put 'Today is: ' now yymmdd10.; ;
run;
Today is: 2019-01-06


Python displays date objects with a default format whereas SAS displays date constants as numeric values.   SAS date constants must always be accompanied by either a SAS-supplied or user-supplied format to output values into a human-readable form.

The Python date object provide attributes such as year,   month,   and day returning year,   month,   and day values respectively.

from datetime import date

nl    = '\n'

d1    = date(1960, 1, 2)
day   = d1.day
month = d1.month
year  = d1.year

 print(nl, 'd1:             '  , d1,
       nl, 'd1 data type:   '  , type(d1),
       nl, 'day:            '  , day,
       nl, 'day data type:  '  , type(day),
       nl, 'month:          '  , month,
       nl, 'month data type:'  , type(month),
       nl, 'year:           '  , year,
       nl, 'year data type: '  , type(year))
d1:              1960-01-02
 d1 data type:    <class 'datetime.date'>
 day:             2
 day data type:   <class 'int'>
 month:           1
 month data type: <class 'int'>
 year:            1960
 year data type:  <class 'int'>


The d1 object is a date object created by calling the date constructor method.   Rules for the date constructor are all arguments are required,   values are integers and must be in the following ranges:

•     1   <=   year   <=   9999

•     1   <=   month   <=   12

•     1   <= day   <=   number of valid days in the given month and year

Illustrate the min,   max,   and   resolution attributes for the earliest,   latest and resolution for a Python date object respectively.

import datetime

nl       = '\n'

early_dt = datetime.date.min
late_dt  = datetime.date.max
res      = datetime.date.resolution

print(nl, 'Earliest Date:' ,early_dt,
      nl, 'Latest Date:  ' ,late_dt,
      nl, 'Resolution:   ' ,res)
Earliest Date: 0001-01-01
Latest Date:   9999-12-31
Resolution:    1 day, 0:00:00


The SAS analog illustrating the YEAR,   MONTH,   and DAY   functions.

data _null_;
   d1    = '2jan1960'd;
   day   = day(d1);
   month = month(d1);
   year  = year(d1);

0 put 'Day:   ' day   /
      'Month: ' month /
      'Year:  ' year;
run;
Day:   2
Month: 1
Year:  1960


Date Manipulation

The Python date class permits the assignment of date values to objects which can be manipulated arithmetically.   For example,   determine the intervening number of days between two dates;   January 2,   1960 and July 4,   2019.   The resulting dif object is a timedelta which we explore below.

from datetime import date

nl  = '\n'

d1  = date(1960, 1, 2)
d2  = date(2019, 7, 4)
dif = d2 - d1

print(nl                    , d1,
      nl                    , d2,
      nl,
      nl, 'Difference:'     , dif,
      nl, 'dif data type:'  , type(dif))
 1960-01-02
 2019-07-04

 Difference: 21733 days, 0:00:00
 dif data type: <class 'datetime.timedelta'>


SAS date constants can used in simple date arithmetic.

data _null_;
   d1  =  '2jan1960'd;
   d2  =  '4jul2019'd;
   dif =  d2 - d1;

   put d1  yymmdd10.  /
       d2  yymmdd10.  /
       dif 'days';
run;
1960-01-02
2019-07-04
21733 days


The Python toordinal function is an alternative for determining the number of intervening days between two dates.

from datetime import date

nl  = '\n'

d1  = date(1960, 1, 2).toordinal()
d2  = date(2019, 7, 4).toordinal()
dif = d2 - d1

print(nl, 'Ordinal for 02Jan1960:' ,d1,
      nl, 'Ordinal for 07Jul2019:' ,d2,
      nl, 'Difference in Days:'    ,dif)
 Ordinal for 02Jan1960: 715511
 Ordinal for 07Jul2019: 737244
 Difference in Days: 21733


Python provisions methods to parse date objects and return constituent components.   Below the dow1 object is assigned the integer value returned by calling the weekday function returning the integer value 2.   The weekday function uses a zero-based (0) index where zero (0) represents Monday,   one (1) represents Tuesday and so on.

from datetime import datetime, date

nl      = '\n'

d1      = datetime(year=2019, month =12, day=25)
dow1    = d1.weekday()

dow2    = d1.isoweekday()
iso_cal = date(2019, 12, 25).isocalendar()

print(nl, 'Day of Week:'     ,dow1,
      nl, 'ISO Day of Week:' ,dow2,
      nl, 'ISO Calendar:'    ,iso_cal)
 Day of Week: 2
 ISO Day of Week: 3
 ISO Calendar: (2019, 52, 3)

In contrast the dow2 object calls the isoweekday function returning integers where integer value one (1) represents Monday and 7 represents Sunday.


The SAS analog calls the WEEKDAY function assigned to the dow variable and returns the value 4 (four).   The WEEKDAY function uses one (1) to represent Sunday,   two (2) to represent Monday,   and so on.

The WEEK function returns an integer value between 1 and 53 indicating the week of the year.   The week1 variable is assigned the value from the call to this function using the d1 variable as its first parameter followed by the optional 'u' descriptor.   This descriptor is the default using Sunday to represent the first day of the week.

The week2 variable is assigned the value from calling the WEEK function followed by the optional 'v' descriptor.   This descriptor uses Monday to represent the first day of the week and includes January 4th and the first Thursday of the year for week 1.   If the first Monday of January is the 2nd,   3rd,   or 4th,   then these days are considered belonging to the week of the preceding year.

data _null_;
   d1  = '25Dec2019'd;
   dow = weekday(d1);
   week1 = week(d1, 'u');
   week2 = week(d1, 'v');
   year  = year(d1);

put 'Day of Week: ' dow /
    'Week1: ' week1     /
    'Week2: ' week2     /
    'Year:  ' year;
run;
Day of Week: 4
Week1: 51
Week2: 52
Year:  2019


Rather than returning integers use the calendar module to return the names for the day of the week.

Call the day_name attribute and supplying integer values between 0 and 6 as the index returns a day by its name.

import calendar
list(calendar.day_name)
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

calendar.day_name[6]
'Sunday'


Return the name of the day from an arbitrary date.

import calendar

nl   = '\n'

dow2 = calendar.weekday(2019, 12, 23)
nod2 = calendar.day_name[dow2]

print(nl, 'Day of Week:' , dow2,
      nl, 'Name of Day:' , nod2)
 Day of Week: 0
 Name of Day: Monday


The SAS analog.

data _null_;
   d2   = '23Dec2019'd;
   dow2 = weekday(d2);
   nod2 = put(d2, weekdate9.);

put 'Day of Week: ' dow2 /
    'Name of Day: ' nod2;
run;
Day of Week: 2
Name of Day: Monday


A common task is determining a date based on duration.   The replace method offers a substitution method for the year,   month,   and day argument for a date object returning a modified date.

from datetime import datetime, date
d1 = date(2018, 12, 31)
if d1 == date(2018, 12, 31):
    d2 = d1.replace(day=25)

print('\n', 'Before replace():', d1,
      '\n', 'After replace(): ', d2)
 Before replace(): 2018-12-31
 After replace():  2018-12-25 


Count days until next birthday.

today = date.today()
birth_day = date(today.year, 1, 24)

if birth_day < today:
   birth_day = birth_day.replace(year=today.year + 1)

days_until = abs(birth_day - today)

print(nl, 'Birthday:'             ,birth_day,
      nl, 'birth_day Data Type:'  ,type(birth_day), nl,
      nl, 'Days until Next:'      ,days_until,
      nl, 'days_until Data Type:' ,type(days_until))
 Birthday: 2020-01-24
 birth_day Data Type: <class 'datetime.date'>

 Days until Next: 240 days, 0:00:00
 days_until Data Type: <class 'datetime.timedelta'>


SAS Analog.

data _null_;

today = today();
birth_day = '24Jan19'd;

if birth_day < today then do;
   next_birth_day = intnx('year', birth_day, 1, 'sameday');
   days_until = abs(next_birth_day - today);
   put 'Next birthday is: ' next_birth_day yymmdd10.;
end;

else do;
   next_birth_day = birth_day;
   put 'Next birthday is: ' next_birth_day yymmdd10.;
   days_until = abs(next_birth_day - today);
end;

      put days_until ' days';
run;


Shift Date

A set of Python functions for shifting dates by a given interval is the paired fromordinal and toordinal methods.   The fromordinal method returns a date object when supplied an integer in the range of allowable Python dates.   The toordinal method returns the integer value from a Python date object.

Shift date 1001 days.

from datetime import datetime, date

nl     = '\n'

d1     = date(2019, 1, 1)
to_ord = d1.toordinal()

inc    = to_ord + 1001
d2     = date.fromordinal(inc)

print(nl, 'd1 Original Date:'    , d1,
      nl, 'd1 Data Type:'        , type(d1), nl,
      nl, 'Shifted Date:'        , d2,
      nl, 'd2 Data Type:'        , type(d2), nl,
      nl, 'to_ord Object:'       , to_ord,
      nl, 'to_ord Data Type:'    , type(to_ord))
 d1 Original Date: 2019-01-01
 d1 Data Type: <class 'datetime.date'>

 Shifted Date: 2021-09-28
 d2 Data Type: <class 'datetime.date'>

 to_ord Object: 737060
 to_ord Data Type: <class 'int'>


SAS analog calling the INTNX function.

data _null_;
   d1 = '01Jan2019'd;
   d2 = intnx('day', d1, 1001);
put 'Original Date: ' d1 yymmdd10. /
    'Shifted Date:  ' d2 yymmdd10.;
run;
Original Date: 2019-01-01
Shifted Date:  2021-09-28


Date Formats

Python date,   datetime,   and time objects use the strftime method to display datetime values by arranging a set of format directives to control the appearances of the output.   The strftime method is analogous to a SAS or user-defined format controling appearances to display SAS datetime constants.

The strftime method converts a Python datetime object to a string.   This is analogous to using the SAS PUT function to convert a SAS datetime constant,   which is numeric,   to a character variable.   The results from calling the strftime method are strings formatted as dates,   times,   or datetimes.

Conversely the strptime method creates a datetime object from a string representing a datetime value using a corresponding format directive to control its appearances.   This is analogous to using the SAS INPUT function and the associated informat to create a character variable from a SAS date constant.   The results from calling the strptime method is a Python datetime object.

A mnemonic for remembering the difference in their behaviors is:

•    strftime is string-format-time

•    strptime is string-parse-time

Table of common Python datetime Format Directives.

Directive Meaning Example
%a Weekday Sun, Mon, . . . , Sat
%A Weekday Full Name Sunday, Monday, . . . , Saturday
%w Weekday as decimal 0, 1, . . . , 6
%d Day of month zero padded 01, 02, . . . , 31
%b Month's Abbreviation Jan, Feb, . . . , Dec
%m Month as zero-padded decimal 01, 02, . . . , 12
%y Year with century as a decimal number 0001, 0002, . . . , 9999
%H 24-hour clock as a zero-padded decimal number 00, 01, . . . , 23
%I 12-hour clock as a zero-padded decimal number 01, 02, . . . , 12
%p AM or PM AM. PM
%M Minute as a zero-padded number 00, 01, . . . , 59
%S Second as a zero-padded number 00, 01, . . . , 59
%Z Time zone name (empty), UTC, EST, CST, etc.
%U Week number of year (Sunday as first day of week) zero padded number 00, 01, . . . , 53
%c Date and time Tue Aug 16 21:30:00 1988
%X Time 12:34:56
%% Literal % character %



Call strftime and strptime methods.

from datetime import date, time, datetime

nl         = '\n'
dt         = date(2019, 1, 24)

dt_str     = dt.strftime('%d%B%Y')
dt_frm_str = datetime.strptime(dt_str, '%d%B%Y')

fmt = '%A, %B %dth, %Y'

print(nl, 'dt Object:'             , dt,
      nl, 'dt Data Type:'          , type(dt),
      nl,
      nl, 'dt_str Object:'         , dt_str,
      nl, 'dt_str Data Type:'      , type(dt_str),
      nl,
      nl, 'dt_frm_str Object:'     , dt_frm_str,
      nl, 'dt_frm_str Data Type:'  , type(dt_frm_str),
      nl,
      nl, 'Display dt_frm_str as:' , dt_frm_str.strftime(fmt),
      nl, "Using Format Directive, '%A, %B %dth, %Y'")
 dt Object: 2019-01-24
 dt Data Type: <class 'datetime.date'>

 dt_str Object: 24January2019
 dt_str Data Type: <class 'str'>

 dt_frm_str Object: 2019-01-24 00:00:00
 dt_frm_str Data Type: <class 'datetime.datetime'>

 Display dt_frm_str as: Thursday, January 24th, 2019
 Using Format Directive, '%A, %B %dth, %Y'


Arbitrary characters can be a part of the strftime function:

dt_frm_str.strftime('%A, %B %dth, %Y')

adding the string th, to the dt_frm_str string object to render:

 Thursday, January 24th, 2019


Date to String

Call the strftime method to convert Python date objects to strings.

from datetime import date, time, datetime, timedelta

nl      = '\n'
d3      = date(2019, 12, 25)

dt_str3 = date.strftime(d3,'%y-%m-%d')
dt_str4 = date.strftime(d3,'%Y-%m-%d')
dt_str5 = date.strftime(d3,'%Y-%B-%d')

print(nl, 'Date Object d3:'    , d3,
      nl, 'd3 Data Type:'      , type(d3),
      nl,
      nl, 'dt_str3:'           , dt_str3,
      nl, 'dt_str3 Data Type:' , type(dt_str3),
      nl,
      nl, 'dt_str4:'           , dt_str4,
      nl, 'dt_str4 Data Type:' , type(dt_str4),
      nl,
      nl, 'dt_str5:'           , dt_str5,
      nl, 'dt_str5 Data Type:' , type(dt_str5))
 Date Object d3: 2019-12-25
 d3 Data Type: <class 'datetime.date'>

 dt_str3: 19-12-25
 dt_str3 Data Type: <class 'str'>

 dt_str4: 2019-12-25
 dt_str4 Data Type: <class 'str'>

 dt_str5: 2019-December-25
 dt_str5 Data Type: <class 'str'>


The SAS analog.   Variable d3 is assigned a date constant and the VTYPE function returns N indcating a numeric variable.   With variable d3 as the first parameter to the PUT function calls the program illustrates different SAS formats to render output similar to the Python format directives.

data _null_;
   length tmp_st $ 16;
    d3        = '25Dec2019'd;
    date_type = vtype(d3);

    dt_str3 = put(d3,yymmdd8.);
    ty_str3 = vtype(dt_str3);

    dt_str4 = put(d3,yymmdd10.);
    ty_str4 = vtype(dt_str4);

    tmp_st  = strip(put(d3,worddatx.));
    dt_str5 = translate(tmp_st,'-',' ');
    ty_str5 = vtype(dt_str3);

   put 'Date Variable d3:  ' d3 date9.  /
       'd3 Date Type:      ' date_type  //
        'dt_str3:           ' dt_str3   /
        'dt_str3 Data Type: ' ty_str3   //
        'd2_str4:           ' dt_str4   /
        'dt_str4 Data Type: ' ty_str4   //
        'dt_str5:           ' dt_str5   /
        'dt_str5 Data Type: ' ty_str5;
run;
Date Variable d3:  25DEC2019
d3 Date Type:      N

dt_str3:           19-12-25
dt_str3 Data Type: C

d2_str4:           2019-12-25
dt_str4 Data Type: C

dt_str5:           25-December-2019
dt_str5 Data Type: C


String Formatting with strftime method.

from datetime import date

d4 = date(2019, 12, 25)

'In 2019, Christmas is on day {0} and falls on {1}'.format(date.strftime(d4, '%j'),date.strftime(d4, '%A'))
'In 2019, Christmas is on day 359 and falls on Wednesday'


String to Date

Do the reverse and convert a Python string object to a datetime object.   The strptime method returns a datetime object from a string.

from datetime import datetime, date

nl     = '\n'
in_fmt = '%Y-%m-%d'

dt_str = '2019-01-01'
d5 = datetime.strptime(dt_str, in_fmt )

print(nl, 'dt_str String:'    , dt_str,
      nl, 'dt_str Data Type:' , type(dt_str),
      nl,
      nl, 'd5 Date is:'       , d5,
      nl, 'd5 Data Type:'     , type(d5))
 dt_str String: 2019-01-01
 dt_str Data Type: <class 'str'>

 d5 Date is: 2019-01-01 00:00:00
 d5 Data Type: <class 'datetime.datetime'>

As the name suggests a datetime object holds values for dates and times discussed below.   Since a time portion is not supplied the resulting d5 datetime object is set to midnight.


The SAS Analog uses the INPUT function to map a date string assigned to the dt_str variable as a date constant.

data _null_;
   dt_str      = '2019-01-01';
   d5          = input(dt_str,yymmdd10.);
   dt_str_type = vtype(dt_str);
   d5_type     = vtype(d5);

put 'Original String: ' dt_str /
    'Data Type: ' dt_str_type  //
    'Date is: ' d5 yymmdd10.   /
    'Data Type: ' d5_type ;
run;
Original String: 2019-01-01
Data Type: C

Date is: 2019-01-01
Data Type: N


Time

A Python time object represents the time of day independent of a particular day subject to change based on the tzinfo object.   The tzinfo object is discussed below in the time zone section.   The time module uses an epoch start,   or offset,   from midnight January 1,   1970.

Return the time module epoch start.

import time
time.gmtime(0)
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)

The gmtime function converts a time value expressed as the number of seconds in this case,   zero (0),   to a Python struct_time object.   In effect,   passing a timestamp value of zero (0) to the gmtime function.   A struct_time object is returned containing a sequence of time and date values accessable by an index values and attribute names.

Table of struct_time index and attributes.

Index Attribute Values
0 tm_year for example,   2019
1 tm_mon range [1,   12]
2 tm_mday range [1,   31]
3 tm_hour range [0,   23]
4 tm_min range [0,   59]
5 tm_sec range [0,   61];   Legacy support for leap-second
6 tm_wday range [0,   6],   Monday is 0
7 tm_yday range [1,   366]

Python time values have a resolution to the microsecond or one millionth of a second,   whereas SAS time vales have a resolution of one second.   Also notice how tm_mon,   tm_mday ,   and tm_yday are one-based (1) indexes rather than the traditional zero-based (0) index.

Call the gmtime function with a timestamp value of zero (0) and return constituent parts of struct_time by attributes and index value.

import time

nl = '\n'
t  = time.gmtime(0)

print(nl, 'Hour:        '  , t.tm_hour,
      nl, 'Minute:      '  , t.tm_min,
      nl, 'Second:      '  , t.tm_sec,
      nl, 'Day of Month:'  , t.tm_mday,
      nl, 'Day of Week: '  , t.tm_wday,
      nl, 'Day of Year: '  , t.tm_yday,
       nl, 'Year:        '  , t[0])
 Hour:         0
 Minute:       0
 Second:       0
 Day of Month: 1
 Day of Week:  3
 Day of Year:  1
 Year:         1970


The SAS analog illustrates the SAS time offset as the number of seconds from midnight January 1,   1960.   It also illustrates several datetime functions to return constituent date and time components.

data _null_;
   t  = '00:00:00't;
  hr  = hour(t);
  mn  = minute(t);
  sc  = second(t);
  mo  = month(t);
  dy  = day(t);
  yr  = year(t);

put "'00:00:00't is: " t datetime32. /
     'Hour:         ' hr /
     'Minute:       ' mn /
     'Second:       ' sc /
     'Day of Month: ' mo /
     'Day of Week:  ' dy /
     'Year:         ' yr;
run;
'00:00:00't is:               01JAN1960:00:00:00
Hour:         0
Minute:       0
Second:       0
Day of Month: 1
Day of Week:  1
Year:         1960


Time of Day

In some cases analysis require consideration of elapsed time with a start time independent of the current day.   For this purpose Python and SAS provision functions to return the current time of day.   These functions call the underlying OS to return time of day from the processor’s clock.

import time

nl      = '\n'
t_stamp = time.time()

now     = time.localtime(time.time())
dsp_now = time.asctime(now)

print(nl, 't_stamp Object:'    , t_stamp,
      nl, 't_stamp Data Type:' , type(t_stamp),
      nl,
      nl, 'now Object:'        , now,
      nl,
      nl, 'dsp_now Object:'    , dsp_now,
      nl, 'dsp_now Data Type:' , type(dsp_now))
 t_stamp Object: 1548867370.6703455
 t_stamp Data Type: <class 'float'>

 now Object: time.struct_time(tm_year=2019, tm_mon=1, tm_mday=30, tm_hour=11, tm_min=56, tm_sec=10, tm_wday=2, tm_yday=30, tm_isdst=0)

 dsp_now Object: Wed Jan 30 11:56:10 2019
 dsp_now Data Type: <class 'str'>

the statement:

now = time.localtime(time.time())

creates the now object by calling the localtime function using the time.time as an argument passing today’s timestamp to the localtime function.   The results is the struct_time object assigned to the now object.

The syntax:

dsp_now = time.asctime(now)

converts the struct_time object (created by the localtime function call) to a string displaying day of week,   date,   time,   and year.   The asctime function converts the Python struct_time object to a string.


The SAS analog uses the SAS DATETIME function returning current date and time as a datetime value.

data _null_;

   now = datetime();
   dow = put(datepart(now), weekdate17.);
   tm  = put(timepart(now), time9.);

   yr  = year(datepart(now));
   bl  = ' ';
   all = cat(tranwrd(dow,',',' '), tm, bl, yr);

put 'all Variable: ' all;
run;
all Variable: Tue  Jan 15  2019 13:50:18 2019


Time Formats

Formatting time values follows the same pattern discussed above using format directives.   The strftime method converts struct_time object into strings representing the constituent parts of time.  

Conversely the strptime method creates a time object from a string representing time values by using a corresponding format directive.   Results from calling the strptime function is a datetime object whose appearance is controlled by the corresponding format directive.

Convert time objects to strings by calling the strftime function.

import time

nl  = '\n'

now = time.localtime(time.time())
n1  = time.asctime(now)

n2  = time.strftime("%Y/%m/%d %H:%M"       , now)
n3  = time.strftime("%I:%M:%S %p"          , now)
n4  = time.strftime("%Y-%m-%d %H:%M:%S %Z" , now)

print(nl, 'Object now:' , now,
      nl,
      nl, 'Object n1:'  , n1,
      nl, 'Object n2:'  , n2,
      nl, 'Object n3:'  , n3,
      nl, 'Object n4:'  , n4)
 Object now: time.struct_time(tm_year=2019, tm_mon=1, tm_mday=30, tm_hour=12, tm_min=21, tm_sec=8, tm_wday=2, tm_yday=30, tm_isdst=0)

 Object n1: Wed Jan 30 12:21:08 2019
 Object n2: 2019/01/30 12:21
 Object n3: 12:21:08 PM
 Object n4: 2019-01-30 12:21:08 Eastern Standard Time


The SAS analog.

data _null_;

   now = datetime();
   dt  = put(datepart(now), weekdate17.);
   tm  = put(timepart(now), time9.);
   yr  = year(datepart(now));

   bl  = ' ';
   n1  = cat(tranwrd(dt,',',' '), tm, bl, yr);
   n2  = cat(put(datepart(now), yymmdds10.), put(timepart(now), time6.));

   n3  = put(timepart(now), timeampm11.);
   n4  = cat(put(datepart(now), yymmddd10.), put(timepart(now), time6.), bl, tzonename());

put 'Variable n1: ' n1 /
    'Variable n2: ' n2 /
    'Variable n3: ' n3 /
    'Variable n4: ' n4;
run;
Variable n1: Wed  Jan 30  2019 12:27:24 2019
Variable n2: 2019/01/30 12:27
Variable n3: 12:27:24 PM
Variable n4: 2019-01-30 12:27 EST


String to Time

Call the strptime function to convert strings to a Python time object.

import time

nl  = '\n'
t   = time.strptime("12:34:56 PM", "%I:%M:%S %p")

hr  = t.tm_hour
min = t.tm_min
sec = t.tm_sec

print(nl, 't Object:   '    , t,
      nl, 't Data Type:'    , type(t),
      nl,
      nl, 'hr Object:   '   , hr,
      nl, 'hr Data Type:'   , type(hr),
      nl,
      nl, 'min Object:   '  , min,
      nl, 'min Data Type:'  , type(min),
      nl,
      nl, 'sec Object:   '  , sec,
      nl, 'sec Data Type:'  , type(sec))
 t Object:    time.struct_time(tm_year=1900, tm_mon=1, tm_mday=1, tm_hour=12, tm_min=34, tm_sec=56, tm_wday=0, tm_yday=1, tm_isdst=-1)
 t Data Type: <class 'time.struct_time'>

 hr Object:    12
 hr Data Type: <class 'int'>

 min Object:    34
 min Data Type: <class 'int'>

 sec Object:    56
 sec Data Type: <class 'int'>

The syntax:

 t = time.strptime("12:34:56 PM", "%I:%M:%S %p")

converts the string "12:34:56 PM" into a struct_time object labeled t.   The attributes tm_hour,   tm_min,   and tm_sec are chained to the t object return integers representing hour,   minute,   and seconds respectively.


The SAS analog.

data _null_;

   t_str = '12:34:56';
   t     = input(t_str,time9.);

   hr    = hour(t);
   hr_t  = vtype(hr);
   min   = minute(t);

   min_t = vtype(min);
   sec   = second(t);
   sec_t = vtype(sec);

put 't Variable:    ' t timeampm11. //
    'hr Variable:   ' hr            /
    'hr Data Type:  ' hr_t          //
    'min Variable:  ' min           /
    'min Data Type: ' min_t         //
    'sec Variable:  ' sec           /
    'sec Data Type: ' sec_t;
run;
t Variable:    12:34:56 PM

hr Variable:   12
hr Data Type:  N

min Variable:  34
min Data Type: N

sec Variable:  56
sec Data Type: N 

The characters '12:34:56' are assigned to the t_str variable.   The INPUT function maps this string as a numeric value assigned to the t variable creating a SAS time constant.   The HOUR,   MINUTE,   and SECOND functions return numerics representing the hour,   minute,   and second respectively.


Datetime

The Python datetime module provides a set of functions and methods for creating,   manipulating,   and formatting datetime objects.   They behave similarly to those from the date and time modules discussed above.

Like the Python date object the datetime object assumes the current Gregorian calendar extended in both directions and like the time object,   datetime objects assume there are exactly 3600 * 24 seconds in every day.   Likewise,   SAS distinguishes between date and time constants,   a SAS datetime constant provides a consistent method for handling a combined date and time value as a single constant.

The Python datetime module provides the datetime constructor method where:

•     minyear   <=   year   <=   maxyear

•     1   <=   month   <=   12

•     1   <=   day   <=   number of days in the given month and year

•     0   <=   hour   <   24

•     0   <=   minute   <   60

•     0   <=   second   <   60

•    0   <=   microsecond   <   1000000

•     tzinfo   =   None

Call the datetime,   datetime.now and the datetime.utcnow,   constructor methods.

import datetime as dt

 nl       = '\n'
dt_tuple = (2018, 10, 24, 1, 23, 45)

dt_obj   = dt.datetime(* dt_tuple[0:5])
dt_now1  = dt.datetime.utcnow()
dt_now2  = dt.datetime.now()

print(nl, 'dt_obj Object:    '  , dt_obj,
      nl, 'dt_obj Data Type: '  , type(dt_obj),
      nl,
      nl, 'dt_now1 Object:   '  , dt_now1,
      nl, 'dt_now1 Data Type:'  , type(dt_now1),
      nl,
      nl, 'dt_now2 Object:   '  , dt_now2,
      nl, 'dt_now2 Data Type:'  , type(dt_now2))
 dt_obj Object:     2018-10-24 01:23:00
 dt_obj Data Type:  <class 'datetime.datetime'>

 dt_now1 Object:    2019-01-30 18:13:15.534488
 dt_now1 Data Type: <class 'datetime.datetime'>

 dt_now2 Object:    2019-01-30 13:13:15.534488
 dt_now2 Data Type: <class 'datetime.datetime'>

The datetime constructor method accepts a tuple of values conforming to the ranges shown above.   The datetime.utc constructor method returns the current date based on the UTC time zone while the datetime.now method returns the current date and local time.  

And because the call to the datetime.now method implies the default tz=None argument this constructor method is the equivalent of calling the datetime.today.   Each of these methods return a datetime object.

Construct the analog SAS datetime constants by building the user-defined py_fmt. format using a PICTURE statement to display SAS datetime constants in the same manner as the default Python datetime objects.

proc format;
   picture py_fmt (default=20)
   low - high = '%Y-%0m-%0d %0I:%0M:%0s' (datatype=datetime);
run;

data _null_;

   dt_str     = '24Oct2018:01:23:45';
   dt_obj     = input(dt_str, datetime20.);
   dt_obj_ty  = vtype(dt_obj);

   dt_now1    = datetime();
   dt_now1_ty = vtype(dt_now1);

   dt_now2     = tzones2u(dt_now1);
   dt_now2_ty  = vtype(dt_now2);;

put 'dt_obj Variable:   '   dt_obj py_fmt.  /
    'dt_obj Data Type:  '  dt_obj_ty        //
    'dt_now1 Variable:  '  dt_now1 py_fmt.  /
    'dt_now1 Data Type: ' dt_now1_ty        //
    'dt_now2 Variable:  '  dt_now2 py_fmt.  /
    'dt_now2 Data Type: ' dt_now2_ty;
run;
dt_obj Variable:  2018-10-24 01:23:45
dt_obj Data Type: N

dt_now1 Variable:  2019-01-30 07:57:27
dt_now1 Data Type: N

dt_now2 Variable:  2019-01-31 12:57:27
dt_now2 Data Type: N 

The DATETIME function is analogous to the Python datetime.now function returning the current datetime from the processor clock.   In order to display today’s datetime based on UTC time zone call the TZONES2U function to convert a SAS datetime to UTC.   Time zones are discussed below.


Combine Dates and Times

There are times when a dataset to be analyzed has columns consisting of separate values for dates and times with the analysis requiring a single datetime column.   To combine the date and time columns use the combine function to form a single datetime object.

from datetime import datetime, date, time

nl      = '\n'
fmt     = '%b %d, %Y at: %I:%M %p'

d1      = date(2019, 5, 31)
t1      = time(12,  34, 56)
dt_comb = datetime.combine(d1,t1)

print(nl, 'd1 Object:'        , d1,
      nl, 't1 Object:'        , t1,
      nl,
      nl, 'dt_comb Object:'    , dt_comb.strftime(fmt),
       nl, 'dt_comb Data Type:' , type(dt_comb))
 d1 Object: 2019-05-31
 t1 Object: 12:34:56

 dt_comb Object: May 31, 2019 at: 12:34 PM
 dt_comb Data Type: <class 'datetime.datetime'>


The SAS analog calls the DHMS function to combine a date and time variables.

data _null_;
   time     = '12:34:56't;
   date     = '31May2019'd;
   new_dt   = dhms(date,12,34,56);

   new_dt_ty = vtype(new_dt);

   put 'new_dt Variable:  ' new_dt py_fmt. /
       'new_dt Data Type: ' new_dt_ty;
run;
new_dt Variable:   2019-05-31 12:34:56
new_dt Data Type: N


Extract Dates and Times

Call the the date and time methods returning a date and time object respectively.   Python datetime objects have the year,   month,   day,   hour,   minute,   and second attributes to return integer values.

import datetime as dt

nl = '\n'
in_fmt = '%Y-%m-%d %I:%M%S'

dt_obj = dt.datetime(2019, 1, 9, 19, 34, 56)
date   = dt_obj.date()
time   = dt_obj.time()

print(nl, 'Date:    '  , date           ,
      nl, 'Time:    '  , time           ,
      nl, 'Year:    '  , dt_obj.year    ,
      nl, 'Month:   '  , dt_obj.month   ,
      nl, 'Day:     '  , dt_obj.day     ,
      nl, 'Hour:    '  , dt_obj.hour    ,
      nl, 'Minute:  '  , dt_obj.minute  ,
      nl, 'Seconds: '  , dt_obj.second)
 Date:     2019-01-09
 Time:     19:34:56
 Year:     2019
 Month:    1
 Day:      9
 Hour:     19
 Minute:   34
 Seconds:  56


The SAS analog calling the DATEPART and TIMEPART functions returning the date and time as date and time constants respectively.

data _null_;

   dt_obj = '09Jan2019:19:34:56'dt;

   date   = datepart(dt_obj);
   time   = timepart(dt_obj);

   year   = year(datepart(dt_obj));
   month  = month(datepart(dt_obj));
   day    =   day(datepart(dt_obj));

   hour   = hour(dt_obj);
   minute = minute(dt_obj);
   second = second(dt_obj);

put 'Date:    ' date yymmdd10.  /
    'Time:    ' time time8.     /
    'Year:    ' year            /
    'Month:   ' month           /
    'Day:     ' day             /
    'Hour:    ' hour            /
    'Minute:  ' minute          /
    'Seconds: ' second;
run;
Date:    2019-01-09
Time:    19:34:56
Year:    2019
Month:   1
Day:     9
Hour:    19
Minute:  34
Seconds: 56


String to Datetime

Converting a Python string representing a string to datetime objects follow the same pattern we saw earlier for converting date and time strings by calling the strptime function.   The strptime function accepts two parameters,   the input string representing a datetime value followed by the format directive matching the input string.

import datetime as dt

nl = '\n'

str    = 'August 4th, 2001 1:23PM'
dt_obj = dt.datetime.strptime(str, '%B %dth, %Y %I:%M%p')

tm     = dt_obj.time()

print(nl, 'str Object:'       , str,
      nl, 'str Data Type:'    , type(str),
      nl,
      nl, 'tm_obj Object:'    , dt_obj,
      nl, 'tm_obj Data Type:' , type(dt_obj),
      nl,
      nl, 'tm Object:'        , tm,
      nl, 'tm Data Type:'     , type(tm))
 str Object: August 4th, 2001 1:23PM
 str Data Type: <class 'str'>

 tm_obj Object: 2001-08-04 13:23:00
 tm_obj Data Type: <class 'datetime.datetime'>

 tm Object: 13:23:00
 tm Data Type: <class 'datetime.time'>


Converting a SAS character variable to a datetime constant is similar to the earlier example in this section for converting character variables to date and time constants.   It this case,   if the SAS variable value of 'August 4, 2001 1:23PM' had a delimiter between the date and time value we can use the ANYDTDTM. informat.

Another approach is to create a user-defined format to match the value’s form.   The steps in this process are:

1.     Create a user-defined informat with PROC FORMAT

2.     Generate the CNTLIN= SAS dataset for PROC FORMAT and designate the TYPE variable in this dataset as I to indicate building an INFORMAT

3.     Call PROC FORMAT to build the user-defined informat by reading the cntlin= input dataset

4.     Call the INPUT function paired with the user-defined informat to create a datetime variable from the character variable value representing the datetime value<br?

proc format;
   picture py_infmt (default=20)
   low - high = '%B %d, %Y %I:%0M%p' (datatype=datetime);
run;

data py_infmt;
   retain fmtname "py_infmt"
          type "I";
   do label = '04Aug2001:00:00'dt to
              '05Aug2001:00:00'dt by 60;
      start  = put(label, py_infmt32.);
      start  = trim(left (start));
      output;
   end;
run;

proc format cntlin = py_infmt;
run;

data _null_;
   str        = 'August 4, 2001 1:23PM';
   dt_obj     = input(str, py_infmt32.);
   dt_obj_ty  = vtype(dt_obj);

   tm         = timepart(dt_obj);
   tm_ty      = vtype(tm);
   yr         = year(datepart(dt_obj));
   yr_ty      = vtype(yr);

put  'dt_obj Variable: '   dt_obj py_infmt32. /
     'dt_obj Data Type: '  dt_obj_ty          //
     'yr Variable: '       yr                 /
     'yr Data Type: '      yr_ty              //
     'tm Variable: '       tm time8.          /
     'tm Data Type: '      tm_ty;
run;
dt_obj Variable: August 4, 2001 1:23PM
dt_obj Data Type: N

yr Variable: 2001
yr Data Type: N

tm Variable: 13:23:00
tm Data Type: N


Datetime to String

Illustrate conversions going the opposite direction:   datetime objects to strings.   Like the examples for time and date objects converting datetime objects to strings calls the strptime function.   Use an f-string containing replacement fields designated by curly braces {   } to compose an expression evaluated at runtime.   A format specifier is appended following the colon (:).

import datetime as dt

nl     = '\n'
fmt    = '%Y-%m-%d %I:%M%S'

dt_obj = dt.datetime(2019, 7, 15, 2, 34, 56)
dt_str = dt_obj.strftime(fmt)

wkdy_str    = f"{dt_obj:%A}"
mname_str   = f"{dt_obj:%B}"
day_str     = f"{dt_obj:%d}"
yr_str      = f"{dt_obj:%Y}"
hr_str      = f"{dt_obj:%I}"
mn_str      = f"{dt_obj:%M}"
sec_str     = f"{dt_obj:%S}"

print(nl                     ,
      'Weekday: ' ,wkdy_str  ,
      nl                     ,
      'Month:   ' , mname_str,
      nl                     ,
      'Day:     ' , day_str  ,
      nl                     ,
      'Year:    ' , yr_str   ,
      nl                     ,
      'Hours:   ' , hr_str   ,
      nl                     ,
      'Minutes: ' , mn_str   ,
      nl,
      'Seconds: ' , sec_str)
 Weekday:  Monday
 Month:    July
 Day:      15
 Year:     2019
 Hours:    02
 Minutes:  34
 Seconds:  56


The SAS analog.

data _null_;

   dt_obj      = '15Jul2019:02:34:56'dt;

   wkdy_str   = put(datepart(dt_obj), downame9.);
   mnname_str = put(datepart(dt_obj), monname.);

   day_str   = put(day(datepart(dt_obj)), 8.);
   yr_str    = put(year(datepart(dt_obj)), 8.);

   hr_str    = cat('0',left(put(hour(dt_obj), 8.)));
   mn_str    = put(minute(dt_obj), 8.);
   sec_str   = put(second(dt_obj), 8.);

put 'Weekday: ' wkdy_str   /
    'Month:   ' mnname_str /
    'Day:     ' day_str    /
    'Year:    ' yr_str     /
    'Hours:   ' hr_str     /
    'Minutes: ' mn_str     /
    'Seconds: ' sec_str    /;
run;
Weekday: Monday
Month:   July
Day:     15
Year:    2019
Hours:   02
Minutes: 34
Seconds: 56


TimeDelta

The TimeDelta method represents a duration between two date or time objects with a granularity of one microsecond.   A rough corollary is the INTCK and INTNX functions.   The INTCK returns the number of datetime intervals that lie between two dates,   times,   or datetimes.   The INTNX function increments or decrements a date,   time,   or datetime value by a given interval and returns a date,   time,   or datetime value.

The timedelta method has the signature:

datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)

where all arguments are options and argument values may be integers or floats either positive or negative.   TimeDelta methods support addition and subtraction operations using time and datetime objects.

Call the TimeDeltamethod to shift back in time the datetime value held by the now object by subtracting a timedelta interval.   Similarly a datetime value is shifted forward in time by adding a timedelta interval to the now object.

import datetime as dt

nl        = '\n'
fmt       = '%b %d %Y %I:%M %p'
now       = dt.datetime.now()

dy_ago1   = now - dt.timedelta(days = 1)
dy_ago2   = now - dt.timedelta(days = 1001)

wk_ago    = now - dt.timedelta(weeks = 1)
yr_fm_now = now + dt.timedelta(weeks = 52)

new_td    = dt.timedelta(days = 730, weeks = 52, minutes = 60)

print (nl, 'Today is:        ' , now.strftime(fmt),
       nl, 'Day ago:         ' , dy_ago1.strftime(fmt),
       nl, '1001 Days ago:   ' , dy_ago2.strftime(fmt),
       nl, '1 Week ago:      ' , wk_ago.strftime(fmt),
       nl, 'In 1 Year:       ' , yr_fm_now.strftime(fmt),
       nl, 'In 3 Yrs, 1 Hour:' , new_td)
 Today is:         Feb 04 2019 03:51 PM
 Day ago:          Feb 03 2019 03:51 PM
 1001 Days ago:    May 09 2016 03:51 PM
 1 Week ago:       Jan 28 2019 03:51 PM
 In 1 Year:        Feb 03 2020 03:51 PM
 In 3 Yrs, 1 Hour: 1094 days, 1:00:00


A common challenge for datetime arithmetic is finding the first and last day of the month.   A timedelta object can be used for finding these dates using the following approach:

1.     Obtain the target date

2.     Find the first day of the month by replacing the day ordinal with the value one (1)

3.     Find the last day of the current month by finding the first day of the succeeding month and subtracting one (1) day

import datetime as dt

nl    = '\n'
fmt   = '%A, %b %d %Y'

date  = dt.date(2016, 2, 2)
fd_mn = date.replace(day=1)

nxt_mn = date.replace(day=28) + dt.timedelta(days=4)
ld_mn  = nxt_mn - dt.timedelta(nxt_mn.day)

print(nl, 'date Object:'       , date,
      nl, 'nxt_mn date:'       , nxt_mn,
      nl, 'Decrement value:'   , nxt_mn.day,
      nl,
      nl, '1st Day of Month:'  , fd_mn.strftime(fmt),
      nl, 'Lst Day of Month:'  , ld_mn.strftime(fmt))
 date Object: 2016-02-02
 nxt_mn date: 2016-03-03
 Decrement value: 3

 1st Day of Month: Monday, Feb 01 2016
 Lst Day of Month: Monday, Feb 29 2016


The SAS analog.

data _null_;

put 'First Day of the Month   Last Day of the Month' /
    '==============================================' /;
do date_idx = '01Jan2019'd to '31Dec2019'd by 31;
   f_day_mo = intnx("month", date_idx, 0, 'Beginning');
   l_day_mo = intnx("month", date_idx, 0, 'End');
    put f_day_mo weekdate22.  l_day_mo weekdate22.;
end;

run;
First Day of the Month   Last Day of the Month
==============================================

      Tue, Jan 1, 2019     Thu, Jan 31, 2019
      Fri, Feb 1, 2019     Thu, Feb 28, 2019
      Fri, Mar 1, 2019     Sun, Mar 31, 2019
      Mon, Apr 1, 2019     Tue, Apr 30, 2019
      Wed, May 1, 2019     Fri, May 31, 2019
      Sat, Jun 1, 2019     Sun, Jun 30, 2019
      Mon, Jul 1, 2019     Wed, Jul 31, 2019
      Thu, Aug 1, 2019     Sat, Aug 31, 2019
      Sun, Sep 1, 2019     Mon, Sep 30, 2019
      Tue, Oct 1, 2019     Thu, Oct 31, 2019
      Fri, Nov 1, 2019     Sat, Nov 30, 2019
      Sun, Dec 1, 2019     Tue, Dec 31, 2019


Extend the logic for finding the first and last day of the month to finding the first and last business day of the month.   We already have the Python program to find the first and last day of the month so they are easily converted into functions called first_day_of_month and last_day_of_month.

Create the functions first_biz_day_of_month returning the first business day of the month and last_biz_day_of_month returning the last business day of the month.   All four functions use a datetime object named any_date with a local scope to define each function definition.

Its role inside the function is two-fold.   First,   receive the year and month input parameters when the functions are called setting an ‘anchor’ date as the first day of the inputs for month and year.   And second is manipulate the ‘anchor’ date with datetime arithmetic to return the appropriate date.

def first_day_of_month(year, month):
   any_date = dt.date(year, month, 1)
   return any_date

def last_day_of_month(year, month):
   any_date = dt.date(year, month, 1)
   nxt_mn = any_date.replace(day = 28) + dt.timedelta(days=4)
   return nxt_mn - dt.timedelta(days = nxt_mn.day)

def first_biz_day_of_month(year, month):
   any_date=dt.date(year, month, 1)
   #If Saturday then increment 2 days
   if any_date.weekday() == 5: 
      return any_date + dt.timedelta(days = 2)        
   #If Sunday increment 1 day
   elif any_date.weekday() == 6: 
       return any_date + dt.timedelta(days = 1)   
   else:
       return any_date

def last_biz_day_of_month(year, month):
    any_date = dt.date(year, month, 1)  
    #If Saturday then decrement 3 days
    if any_date.weekday() == 5:
        nxt_mn = any_date.replace(day = 28) + dt.timedelta(days = 4)
        return nxt_mn - dt.timedelta(days = nxt_mn.day) \
           - abs((dt.timedelta(days = 1)))
    #If Sunday then decrement 3 days
    elif any_date.weekday() == 6:
        return nxt_mn - dt.timedelta(days = nxt_mn.day) \
           - abs((dt.timedelta(days = 2)))
    else:
        nxt_mn = any_date.replace(day = 28) + dt.timedelta(days = 4)
        return nxt_mn - dt.timedelta(days = nxt_mn.day)

Finding the last business day of the month involves handling three conditions:   default condition,   Saturday condition,   and Sunday condition.

  1.     Default Condition.

  a.      Create the local nxt_mn datetime object as the logical last day of the month as the 28th to handle the month of February using the replace function.

   b.     Add a timedelta of 4 days to the local nxt_mn datetime object ensuring the month for this date value is the month suceeding the month of the ‘anchor’ date.   By replace this day ordinal with 28 and adding 4 days you have the first day of the suceeding month.   Subtracting 1 day returns the last day of the ‘anchor’ month.

  2.      The Saturday condition uses the same logic as the default condition and includes:

  a.     Use of the weekday function to test if the returned ordinal is 5.   When True decrement the local nxt_mn datetime object by one day to the preceeding Friday.

  3.      The Sunday condition uses the same logic as the default condition and includes:

  a.      Use of the weekday function to test if the returned ordinal is 6.   When True decrement the local nxt_mn datetime object by two days to the preceeding Friday.


Call the four functions with a year and month parameter,   in this case February 2020.

nl    = '\n'
fmt   = '%A, %b %d %Y'
year  = 2020
month = 2

print(nl, '1st Day    :' , first_day_of_month(year, month).strftime(fmt),
      nl, '1st Biz Day:' , first_biz_day_of_month(year, month).strftime(fmt),
      nl,
      nl, 'Lst Day    :' , last_day_of_month(year, month).strftime(fmt),
      nl, 'Lst Biz Day:' , last_biz_day_of_month(year, month).strftime(fmt))
 1st Day    : Saturday, Feb 01 2020
 1st Biz Day: Monday, Feb 03 2020

 Lst Day    : Saturday, Feb 29 2020
 Lst Biz Day: Friday, Feb 28 2020


The SAS analog uses the WEEKDAY function to return the ordinal day of the week.   Recall the WEEKDAY function returns 1 for Sunday,   2 for Monday,   and so on.

data _null_;
   put 'First Biz Day of Month   Last Biz Day of Month' /
       '==============================================' /;
do date_idx  = '01Jan2019'd to '31Dec2019'd by 31;
   bf_day_mo = intnx("month", date_idx, 0, 'Beginning');
   bl_day_mo = intnx("month", date_idx, 0, 'End');

   beg_day  = weekday(bf_day_mo);
   end_day  = weekday(bl_day_mo);

   /* If Sunday increment 1 day */
   if beg_day = 1 then bf_day_mo + 1;
   /* if Saturday increment 2 days */
      else if beg_day = 7 then bf_day_mo + 2;

   /* if Sunday decrement 2 days */
   if end_day = 1 then bl_day_mo + (-2);
   /* if Saturday decrement 1 */
      else if end_day = 7 then bl_day_mo + (-1);

put bf_day_mo weekdate22.  bl_day_mo weekdate22. ;
end;
run;
First Biz Day of Month   Last Biz Day of Month
==============================================

      Tue, Jan 1, 2019     Thu, Jan 31, 2019
      Fri, Feb 1, 2019     Thu, Feb 28, 2019
      Fri, Mar 1, 2019     Fri, Mar 29, 2019
      Mon, Apr 1, 2019     Tue, Apr 30, 2019
      Wed, May 1, 2019     Fri, May 31, 2019
      Mon, Jun 3, 2019     Fri, Jun 28, 2019
      Mon, Jul 1, 2019     Wed, Jul 31, 2019
      Thu, Aug 1, 2019     Fri, Aug 30, 2019
      Mon, Sep 2, 2019     Mon, Sep 30, 2019
      Tue, Oct 1, 2019     Thu, Oct 31, 2019
      Fri, Nov 1, 2019     Fri, Nov 29, 2019
      Mon, Dec 2, 2019     Tue, Dec 31, 2019


Time Zone

Naïve and Aware Datetimes

The Python Standard Library for datetime has two types of date and datetime objects;   ‘naïve’ and ‘aware’.   The Python date and datetime examples used above are ‘naïve’,   leaving it up to the program logic to determine context for date and datetime value represention.

Here we cover the Python timezone object used for representing time and datetime objects with an offset from UTC.   We also introduce the pytz module to provide cross-time zone time and datetime handling.   The pytz module augments ‘aware’ time and datetime objects by exposing an interface to the tz database managed by the Internet Corporation for Assigned Names and Numbers,   or ICANN.

The tz database is a collection of rules for civil time adjustments around the world.   As a general practice,   datetime functions and arithmetic should be conducted using the UTC time zone and converted to the local time zone for human-readable output.

No daylight savings time occurs in UTC time zone,   not to mention that nearly all scientific datetime measurements use UTC.   A datetime object is ’naïve’ if its tzinfo attribute returns None.

As a best practice an application needing the current time should request the current UTC time.   In today’s virtualized and cloud-based server environments requesting the local time is often determined by the physical location of the servers,   which themselves cab be in locations around the world.   Calling the utcnow function to return the UTC datetime eliminates this uncertainty.

import datetime as dt

nl            = '\n'
fmt           = "%Y-%m-%d %H:%M:%S (%Z)"

dt_local      = dt.datetime.now()
dt_naive      = dt.datetime.utcnow()
dt_aware      = dt.datetime.now(dt.timezone.utc)

print(nl, 'naive dt_local:     ' , dt_local.strftime(fmt),
      nl, 'tzinfo for dt_local:' , dt_local.tzinfo,
      nl,
      nl, 'naive  dt_naive:    ' , dt_naive.strftime(fmt),
      nl, 'tzinfo for dt_naive:' , dt_naive.tzinfo,
      nl,
      nl, 'aware  dt_aware:    ' , dt_aware.strftime(fmt),
      nl, 'tzinfo for dt_aware:' , dt_aware.tzinfo)
 naive dt_local:      2019-02-02 18:02:29 ()
 tzinfo for dt_local: None

 naive  dt_naive:     2019-02-02 23:02:30 ()
 tzinfo for dt_naive: None

 aware  dt_aware:     2019-02-02 23:02:30 (UTC)
 tzinfo for dt_aware: UTC


This example converts the dt_str object to the dt_naive datetime object calling the strptime method.   Then call the replace method to replace the None value for the tzinfo attribute for the dt_obj_naive object to UTC and keeping the remainder of the datetime object constituent values and assigning this results to the dt_obj_aware object.

from datetime import datetime, timezone

nl       = '\n'
fmt      = "%Y-%m-%d %H:%M:%S (%Z)"
infmt    = "%Y-%m-%d %H:%M:%S"

dt_str   = '2019-01-24 12:34:56'
dt_naive = datetime.strptime(dt_str, infmt )
dt_aware = dt_naive.replace(tzinfo=timezone.utc)

print(nl, 'dt_naive:           '  , dt_naive.strftime(fmt),
      nl, 'tzinfo for dt_naive:'  , dt_naive.tzinfo,
      nl,
      nl, 'dt_aware:           '  , dt_aware.strftime(fmt),
      nl, 'tzinfo for dt_aware:'  , dt_aware.tzinfo)
 dt_naive:            2019-01-24 12:34:56 ()
 tzinfo for dt_naive: None

 dt_aware:            2019-01-24 12:34:56 (UTC)
 tzinfo for dt_aware: UTC

As a result the tzinfo attribute for the dt_aware object returns UTC.


pytz Library

There are cases where ‘naïve’ objects need converting to ‘aware’ with time zone attributes other than UTC.   To do this utilize a the pytz library.   Convert the datetime string 2019-01-24 12:34:56 having no timezone attribute to to an ‘aware’ datetime object for the US Eastern timezone.   The pytz library provisions the timezone as a method modifying a datetime object's tzinfo attribute suppling a time zone designation.

from datetime import datetime
from pytz import timezone

nl       = '\n'
in_fmt   = "%Y-%m-%d %H:%M:%S"
fmt      = '%b %d %Y %H:%M:%S (%Z) %z'

dt_str   = '2019-01-24 12:34:56'
dt_naive = datetime.strptime(dt_str, in_fmt)
dt_est   = timezone('US/Eastern').localize(dt_naive)

print(nl, 'dt_naive:          ' , dt_naive.strftime(fmt),
      nl, 'tzino for dt_naive:' , dt_naive.tzinfo,
      nl,
      nl, 'datetime dt_est:   ' , dt_est.strftime(fmt),
      nl, 'tzinfo for dt_est: ' , dt_est.tzinfo)
 dt_naive:           Jan 24 2019 12:34:56 ()
 tzino for dt_naive: None

 datetime dt_est:    Jan 24 2019 12:34:56 (EST) -0500
 tzinfo for dt_est:  US/Eastern


A question arising is where to find the list of valid time zone values.   The pytz library provides data structures to return this information.   One structure is the common_timezones which is returned as a list shown below.   At the time of this writing it returns 439 valid timezone values available to the timezone function.

import random
from pytz import common_timezones
print('\n', len(common_timezones), 'entries in the common_timezone list', '\n')
 439 entries in the common_timezone list


Select a random sample of 10 rows from the common_timezones list.

print('\n'.join(random.sample(common_timezones, 10)))
America/Martinique
America/Costa_Rica
Asia/Ust-Nera
Asia/Dhaka
Africa/Mogadishu
America/Maceio
America/St_Barthelemy
Pacific/Bougainville
America/Blanc-Sablon
America/New_York


Another list is the country_timezones.   By supplying an ISO-3166 two letter country code the time zones in use by the country is returned.   Illustrate returning the Swiss timezone from this list.

import pytz
print('\n'.join(pytz.country_timezones['ch']))
Europe/Zurich


Another example.

from pytz import country_timezones
print('\n'.join(country_timezones('ru')))
Europe/Kaliningrad
Europe/Moscow
Europe/Simferopol
Europe/Volgograd
Europe/Kirov
Europe/Astrakhan
Europe/Saratov
Europe/Ulyanovsk
Europe/Samara
Asia/Yekaterinburg
Asia/Omsk
Asia/Novosibirsk
Asia/Barnaul
Asia/Tomsk
Asia/Novokuznetsk
Asia/Krasnoyarsk
Asia/Irkutsk
Asia/Chita
Asia/Yakutsk
Asia/Khandyga
Asia/Vladivostok
Asia/Ust-Nera
Asia/Magadan
Asia/Sakhalin
Asia/Srednekolymsk
Asia/Kamchatka
Asia/Anadyr


Above we used the datetime replace method to replace the tzinfo attribute with UTC returning a datetime ‘aware’ object.   What happens when you use this approach with the pytz library?   Unfortunately,   it does not work.

from datetime import datetime, timedelta
from pytz import timezone
import pytz

nl       = '\n'
fmt      = "%Y-%m-%d %H:%M:%S (%Z) %z"

new_york = timezone('US/Eastern')
shanghai = timezone('Asia/Shanghai')

dt_loc   = new_york.localize(datetime(2018, 12, 23, 13, 0, 0))
dt_sha   = dt_loc.replace(tzinfo=shanghai)
tm_diff   = dt_loc - dt_sha
tm_bool  = dt_loc == dt_sha

print(nl, 'dt_loc datetime: ' , dt_loc.strftime(fmt),
      nl, 'dt_loc tzinfo:   ' , dt_loc.tzinfo,
      nl,
      nl, 'dt_sha datetime: ' , dt_sha.strftime(fmt),
      nl, 'dt_sha tzinfo:   ' , dt_sha.tzinfo,
      nl,
      nl, 'Time Difference: ' ,tm_diff)
      nl, 'dt_loc == dt_sha:' , tm_bool)
dt_loc datetime: 2018-12-23 13:00:00 (EST) -0500
dt_loc tzinfo:   US/Eastern

dt_sha datetime: 2018-12-23 13:00:00 (LMT) +0806
dt_sha tzinfo:   Asia/Shanghai

Time Difference: 13:06:00
dt_loc == dt_sha: False

In this case the dt_sha object is an ‘aware’ object but returns the local mean time (LMT) with a time difference between New York and Shanghai of 13 hours and six minutes.   Obviously,   if both times are the same times represented by different time zones, there is no time difference.

In order to properly convert a time from one time zone to another use the datetime astimezone method instead.   The astimezone method returns an ‘aware’ datetime object adjusting date and time to UTC time and reporting it in the local time zone.

from datetime import datetime, timedelta
from pytz import timezone
import pytz

nl       = '\n'
fmt      = "%Y-%m-%d %H:%M:%S (%Z) %z"
new_york = timezone('US/Eastern')
shanghai = timezone('Asia/Shanghai')

dt_loc   = new_york.localize(datetime(2018, 12, 23, 13, 0, 0))

dt_sha2  = dt_loc.astimezone(shanghai)
tm_diff  = dt_loc - dt_sha2
tm_bool  = dt_loc == dt_sha2

print(nl, 'dt_loc datetime: '  , dt_loc.strftime(fmt),
      nl, 'dt_sha2 datetime:'  , dt_sha2.strftime(fmt),
      nl,
      nl, 'Time Difference: '  , tm_diff,
      nl, 'dt_loc == dt_sha2:' , tm_bool)
 dt_loc datetime:  2018-12-23 13:00:00 (EST) -0500
 dt_sha2 datetime: 2018-12-24 02:00:00 (CST) +0800

 Time Difference:  0:00:00
 dt_loc == dt_sha2: True


For datetime manipulation with local datetimes use the normalize method in order to properly handle daylight savings and standard time transitions.   The dt_loc object calls the localize method to create a datetime object for 1:00 am,   November 4th,   2018,   which is the U.S. transition date and time from daylight savings to standard time.

from datetime import datetime, timedelta
from pytz import timezone
import pytz

nl         = '\n'
fmt        = "%Y-%m-%d %H:%M:%S (%Z) %z"
new_york   = timezone('US/Eastern')

 dt_loc    = new_york.localize(datetime(2018, 11, 4, 1, 0, 0))
 minus_ten = dt_loc - timedelta(minutes=10)

before     = new_york.normalize(minus_ten)
after      = new_york.normalize(before + timedelta(minutes=20))

print(nl, 'before:' , before.strftime(fmt),
      nl, 'after: ' , after.strftime(fmt))
 before: 2018-11-04 01:50:00 (EDT) -0400
 after:  2018-11-04 01:10:00 (EST) -0500

The before datetime object is created using the normalize method to subtract a timedelta of 10 minutes from the dt_loc datetime object returning a datetime for Eastern Daylight Time.   Adding a timedelta of 20 minutes to the before datetime object returns a datetime for Eastern Standard Time.


Illustrates the wrong way to find a duration between two datetimes when the duration includes the transition date from daylight savings to standard time.

nl           = '\n'
fmt          = "%Y-%m-%d %H:%M:%S (%Z) %z"
new_york     = timezone('US/Eastern')

tm_end       = new_york.localize(datetime(2018, 11, 8, 0, 0, 0))

tm_start_est = tm_end - timedelta(weeks=1)

print(nl, 'Datetime arithmetic using local time zone',
      nl, 'tm_start_est: ' , tm_start_est.strftime(fmt),
      nl, 'tm_end:       ' , tm_end.strftime(fmt))
 Datetime arithmetic using local time zone
 tm_start_est:  2018-11-01 00:00:00 (EST) -0500
 tm_end:        2018-11-08 00:00:00 (EST) -0500


Illustrates the correct way to find a duration between two datetimes when the duration includes the transition date from daylight savings to standard time.

tm_end_utc   = tm_end.astimezone(pytz.utc)

tm_delta_utc = tm_end_utc - timedelta(weeks=1)

tm_start_edt = tm_delta_utc.astimezone(new_york)

print(nl, 'Datetime arithmetic using UTC time zone',
      nl, 'tm_start_edt: ' , tm_start_edt.strftime(fmt),
      nl, 'tm_end :      ' , tm_end.strftime(fmt))
 Datetime arithmetic using UTC time zone
 tm_start_edt:  2018-11-01 01:00:00 (EDT) -0400
 tm_end :       2018-11-08 00:00:00 (EST) -0500

The preferred approach is convert the tm_end datetime object to the UTC time zone,   subtract a timedelta of one (1) week,   then convert the new datetime object back to the US Eastern time zone.


SAS Timezone

The SAS time zones is handled by setting options for the execution environment using the option TIMEZONE.   Unless otherwise set,   the default value is BLANK indicating that SAS uses the time zone value called from the underlying operating system.   In this case a PC running Windows 10 with the time zone set to U.S. Eastern.

Beginning with release 9.4 SAS implemented a series of time zone options and functions,   one of which is the TZONENAME function returning the local time zone in effect for the SAS execution environment.   In the example below it returns a blank indicating the SAS TIMEZONE options is not set.   Some SAS environment may have this option set as a restricted option and cannot be overridden.

proc options option=timezone;
run;
    SAS (r) Proprietary Software Release 9.4  TS1M5

 TIMEZONE=         Specifies a time zone.


Call the DATETIME function to return the current date and time.

data _null_;
   local_dt = datetime();
   tz_name  = tzonename();
put 'Default Local Datetime: ' local_dt e8601LX. /
    'Default Timezone:       ' tz_name;
run;
Default Local Datetime:  2019-02-04T10:54:52-05:00
Default Timezone:

Despite not having set an explicit TIMEZONE option,   calling the DATETIME function returns a datetime representing the local time for the Eastern US,   indicated by the ISO-8601 e8601LX. datetime format.   It outputs the datetime and appends the time zone difference between the local time and UTC.

Setting the SAS TIMEZONE option impacts the behaviors of these datetime functions:
  •     DATE

  •     DATETIME

  •     TIME

  •     TODAY

Also impacted by the SAS TIMEZONE options are these SAS time zone functions:
  •     TZONEOFF

  •     TZONEID

  •     TZONENAME

  •    
TZONES2U

  •     TZONEU2S

And the following time zone related formats are impacted:
  •     B8601DXw.

  •     E8601DXw.

  •     B8601LXw.

  •     E8601LXw.

  •     B8601TXw.

  •     E8601TXw.

  •     NLDATMZw.

  •     NLDATMTZw.

  •     NLDATMWZw.

The following are some of the Time Zone function names and their behaviors used in the examples below:

Name Returns Example
TZONEID Time Zone ID ASIA/TOKYO
TZONENAME Current standard or daylight savings time zone name CDT
TZONEDSTNAME Daylight savings time name PDT
TZONESTTNAME Standard time name EST
TZONEDSTOFF Time zone offset from UTC for specified daylight savings time 18000
TZONESTTOFF Time zone offset from UTC for specified standard time -25200

In cases where Daylight Savings is not observed,   for example,   China,   the the TZONEDTNAME function returns a blank and the TZONEDSTOFF returns missing (.)

Illustrate SAS time zone functions.

options tz='America/New_York';
data _null_;
   tz_ny_id       = tzoneid();

   tz_ny_name     = tzonename();

   tz_ny_dst_name = tzonedstname();

   tz_ny_st_name  = tzonesttname();

   tz_ny_off      = tzoneoff();

   tz_ny_off_dst  = tzonesttoff();

   tz_ny_off_st   = tzonedstoff();

put 'TZ ID:                 ' tz_ny_id       /
    'TZ Name:               ' tz_ny_name     /
    'Daylight Savings Name: ' tz_ny_dst_name /
    'Standard Time Name:    ' tz_ny_st_name   //
    'TZ Offset from UTC:    ' tz_ny_off      /
    'TZ DST Offset from UTC ' tz_ny_off_st   /
    'TZ STD Offset from UTC ' tz_ny_off_dst  /;
run;
TZ ID:                 AMERICA/NEW_YORK
TZ Name:               EST
Daylight Savings Name: EDT
Standard Time Name:    EST

TZ Offset from UTC:    -18000
TZ DST Offset from UTC -14400
TZ STD Offset from UTC -18000  


Call the TZONEOFF function to find difference between time zones.   Since time zone offsets west of the prime meridian (UTC location) are negative and those east are positive,   take the difference between the UTC offsets and then take its absolute value.

options tz='';

data _null_;

local_utc_offset = tzoneoff();

dn_frm_utc       = tzoneoff('America/Denver');
mo_frm_utc       = tzoneoff('Africa/Mogadishu');

diff_tz_hr       = abs((dn_frm_utc) - (mo_frm_utc)) / 3600;

put 'Denver UTC Offset:  ' dn_frm_utc    /
    'Mogadishu UTC Offset: ' mo_frm_utc    //
    'Timezone Difference:  ' diff_tz_hr ' Hours' /
     'Local UTC Offset:     ' local_utc_offset;
run;
Denver UTC Offset:  -25200
Mogadishu UTC Offset: 10800

Timezone Difference:  10  Hours
Local UTC Offset:     -18000


Whether an instance of SAS has the TIMEZONE option set,   or is implied,   the TZONEOFF function returns the number of seconds between the local time zone (obtained from the OS when not explicitly set) and UTC.   In this case,   the local UTC offset is for the Eastern US.

What happens to a SAS datatime constant created with one TIMEZONE (alias is TZ) option in effect and subsequently read with a instance of SAS using a different TZ option?

Create the ny dataset with a datatime constant with a SAS instance setting TZ option to 'America/New_York'.

%let dt_fmt = dateampm.;
options tz='America/New_York';
data ny;
   ny_dt = datetime();
   ny_tz = tzonename();

put 'Time Zone in Effect: ' ny_tz /
    'Local Date Time:     ' ny_dt &dt_fmt;
run;

Time Zone in Effect: EST
Local Date Time:     05FEB19:03:24:08 PM

NOTE: The data set WORK.NY has 1 observations and 2 variables.


Create the sha dataset with a datatime constant with a SAS instance setting TZ option to tz='Asia/Shanghai'.

options tz='Asia/Shanghai';
data sha;
   sha_dt = datetime();
   sha_tz = tzonename();

put 'Time Zone in Effect: ' sha_tz /
     'Local Date Time:    ' sha_dt &dt_fmt;
run;
Time Zone in Effect: CST
Local Date Time:     06FEB19:04:24:08 AM

NOTE: The data set WORK.SHA has 1 observations and 2 variables.


Change the tz option to 'America/New_York' MERGE the ny and sha datasets and take the absolute difference between the two datatime constants.

options tz='America/New_York';
data both;
   merge ny
         sha;

diff_tz = abs(tzoneoff('America/New_york', ny_dt) -
               tzoneoff('Asia/Shanghai', sha_dt)) /3600;

put 'New York Datetime Read:     ' ny_dt   &dt_fmt  /
    'Shanghai Datetime Read:     ' sha_dt  &dt_fmt / /
    'Time Difference NY and SHANGHAI: ' diff_tz ' Hours';

run;
New York Datetime Read:      05FEB19:03:24:08 PM
Shanghai Datetime Read:      06FEB19:04:24:08 AM
Time Difference NY and SHANGHAI: 12  Hours

As you would expect, writing datetime constants with one TZ option in effect and subsequently reading them with a different TIMEZONE option does not alter their values.


Illustrate converting datetime constants from one timezone to another.

Create the sha dataset with two datetime values set to midnight,   03Nov2019 and 04Nov2019 with the TZ option set to 'Asia/Shanghai'.   November 4th,   2019 is the date for the transition from Daylight savings to Standard time in the U.S.

%let dt_fmt = dateampm.;

options tz='Asia/Shanghai';
data sha;
   do sha_dt_loc = '03Nov2019:00:00'dt to
                   '04Nov2019:00:00'dt by 86400;
      output;
      put 'Shanghai Datetime Local:  ' sha_dt_loc &dt_fmt;
   end;
run;
Shanghai Datetime Local:  03NOV19:12:00:00 AM
Shanghai Datetime Local:  04NOV19:12:00:00 AM

NOTE: The data set WORK.SHA has 2 observations and 1 variables.


Create the sha dataset by reading the ny dataset with the TZ option set to 'Asia/Shanghai'. Caclulate the differences between the two time zones calling the TZONEOFF function.   Convert the Shanghai datetime constants to New York datetime constants by calling the INTNX function.   Since the timezone differences are calculated using the TZONEOFF function,   the values are 'aware' of the transition to standard time in the U.S.

options tz='America/New_York';
data ny;
   set sha;

ny_utc      = tzoneoff('America/New_york',
              sha_dt_loc)/3600;

sha_utc     = tzoneoff('Asia/Shanghai', sha_dt_loc)/3600;

diff_tz     = abs(tzoneoff('America/New_york', sha_dt_loc) -
                  tzoneoff('Asia/Shanghai', sha_dt_loc)) ;

sha_2_ny_tm = intnx('seconds', sha_dt_loc, diff_tz, 'same');

diff_tz_hr  = diff_tz / 3600;

put 'Sha Local DT Read:    ' sha_dt_loc   &dt_fmt /
    'Sha DT to NY DT:      ' sha_2_ny_tm  &dt_fmt /
    'Time zone Difference: ' diff_tz_hr /
    'New York UTC Offset:  ' ny_utc /
    'Shanghai UTC Offset:  ' sha_utc //;
run;
Sha Local DT Read:    03NOV19:12:00:00 AM
Sha DT to NY DT:      03NOV19:12:00:00 PM
Time zone Difference: 12
New York UTC Offset:  -4
Shanghai UTC Offset:  8


Sha Local DT Read:    04NOV19:12:00:00 AM
Sha DT to NY DT:      04NOV19:01:00:00 PM
Time zone Difference: 13
New York UTC Offset:  -5
Shanghai UTC Offset:  8