© Randy Betancourt, Sarah Chen 2019
R. Betancourt, S. ChenPython for SAS Usershttps://doi.org/10.1007/978-1-4842-5001-3_7

7. Date and Time

Randy Betancourt1  and Sarah Chen2
(1)
Chadds Ford, PA, USA
(2)
Livingston, NJ, USA
 

Date and time data can be confusing because there are so many different formats that can mean the same or different values, depending on where we are and what we do. Date and time value handling are important in every field and can be critically important calculating durations, to GPS triangulation, your Uber ride, and whether you are able to schedule a meeting with colleagues in different parts of the world. Further, as we will see, languages like Python and SAS use different offsets for calendrical functions. Additional confusion often arises as a result of manipulating date and datetime values originating in the different time zone around the world.

Different Python modules define a variety of date and time objects and functions. In this chapter we start with Python’s built-in datetime module. The topics covered here are
  • date

  • time

  • datetime

  • timedelta

  • tzinfo

We will also briefly cover useful third-party date-related modules.

Date Object

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

In contrast, 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 1 second.

Return Today’s Date

We begin the discussion by reviewing both the Python and SAS today() functions to return the current local date. Begin with Listing 7-1.
>>> from datetime import date
>>> now = date.today()
>>> print(' ', 'Today is:     ', now,
...       ' ', 'now Data Type:', type(now))
 Today is:      2019-01-06
 now Data Type: <class 'datetime.date'>
Listing 7-1

Python today() Function

For Python, the now object is created by calling the today() function returning the current date that is printed using a default display format. Later in this chapter, we will examine how to alter the default display format. The now object is a datetime object. A Python datetime object is subject to date and time manipulation using both simple and complex methods. A Python date object is analogous to a SAS date constant.

Similarly, both Python and SAS expose numerous functions for manipulating date values. The SAS TODAY function behaves in the same manner as its Python counterpart, returning the current date. Listing 7-2 illustrates the same logic as Listing 7-1.
4 data _null_;
5    now = today();
6    put 'Today is: ' now yymmdd10.; ;
7 run;
OUTPUT:
Today is: 2019-01-06
Listing 7-2

SAS TODAY Function

Another difference between Python and SAS is Python displays date objects with a default format, whereas the 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 default Python date format corresponds to the SAS-supplied yymmdd10.format.

The Python date object provides attributes such as .year, .month, and .day to return the year, month, and day values, respectively. These attributes return integer values. These attributes are similar to the SAS functions YEAR, MONTH, and DAY used to return the year, month, and day, respectively, from SAS date constants. See Listing 7-3.
>>> from datetime import date
>>>
>>> nl    = ' '
>>>
>>> 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'>
Listing 7-3

Python Date Attributes

The d1 object is a date object created by calling the date() constructor. The rules for the date() constructor are all arguments are required and values are integers, and they must be in the following ranges:
  • 1 <= year <= 9999

  • 1 <= month <= 12

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

Listing 7-4 illustrates the .min, .max, and .res attributes for the earliest, latest, and resolution for a Python date object, respectively.
>>> import datetime
>>>
>>> nl       = ' '
>>>
>>> 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
Listing 7-4

Python Date Range

The analog to Listing 7-4 is the SAS program shown in Listing 7-5. This program illustrates the YEAR, MONTH, and DAY functions. Recall SAS date constants are enclosed in quotes (similar to strings) with “D” or ‘d’ concatenated to the end of the value to indicate a literal date constant. The “data type” for a SAS date literal is numeric and represents an offset from January 1, 1960.
4 data _null_;
5   d1    = '2jan1960'd;
6   day   = day(d1);
7   month = month(d1);
8   year  = year(d1);
9
10 put 'Day:   ' day   /
12     'Month: ' month /
13     'Year:  ' year;
14   run;
OUTPUT:
Day:   2
Month: 1
Year:  1960
Listing 7-5

SAS Date Functions

Date Manipulation

The Python date class permits the assignment of date values to objects which can be manipulated arithmetically. Consider Listing 7-6. Here we wish to determine the intervening number of days between two dates: January 2, 1960, and July 4, 2019.
>>> from datetime import date
>>>
>>> nl  = ' '
>>>
>>> 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'>
Listing 7-6

Python Date Manipulation

In this example, objects d1 and d2 are date objects formed by calling the date() constructor . As date objects, d1 and d2 can be utilized in date arithmetic such as calculating an interval between two dates. The resulting dif object is a Timedelta which we explore later in the chapter. In this case, there are 21,733 days between the two dates.

Listing 7-7 is the analog to Listing 7-6. Given SAS date constants are offsets from January 1, 1960, they can be used in simple date arithmetic.
4 data _null_;
5   d1  =  '2jan1960'd;
6   d2  =  '4jul2019'd;
7   dif =  d2 - d1;
8
9  put d1  yymmdd10.  /
10     d2  yymmdd10.  /
11     dif 'days';
12 run;
OUTPUT:
1960-01-02
2019-07-04
21733 days
Listing 7-7

SAS Date Manipulation

In this example, SAS variables d1 and d2 are assigned arbitrary date literal constants. The dif variable returns the number of days between these two dates.

Like any language, multiple methods can be used to solve a given problem. As an example, consider Listing 7-8 which is an alternative for determining the number of intervening days between two dates, similar to Listing 7-6.

This example introduces the toordinal() method to create the d1 and d2 objects, as integers returned from the method call and used to calculate intervening days between two dates.
>>> from datetime import date
>>>
>>> nl  = ' '
>>>
>>> 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
Listing 7-8

Python toordinal() Function

The toordinal() method returns the ordinal value for January 2, 1960, and assigns it to the d1 object. Likewise, the toordinal() method returns the ordinal for July 4, 2019, and assigns it to the d2 object. The dif object is assigned the difference between the d1 and d2 object. The result is the number of days between these two dates. Unlike Listing 7-6, the type for the dif object is an int rather than returning a Timedelta.

Python provisions methods to parse date objects and return constituent components. Consider Listing 7-9. In this example, the dow1 object is assigned the integer value returned by calling the weekday() function , in this case, 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      = ' '
>>>
>>> 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)
Listing 7-9

Python weekday() Function

Where the dow1 object calls the weekday() function , in contrast, the dow2 object calls the isoweekday() function returning integers where integer value one (1) represents Monday and 7 represents Sunday.

Illustrated in this example is the .isocalendar attribute which parses the d1 date object to return a three-tuple value for the ISO year, ISO week number, and ISO day of the week.

The analog SAS program is shown in Listing 7-10.
4  data _null_;
5     d1  = '25Dec2019'd;
6     dow = weekday(d1);
7     week1 = week(d1, 'u');
8     week2 = week(d1, 'v');
9     year  = year(d1);
10
12 put 'Day of Week: ' dow /
13     'Week1: ' week1     /
14     'Week2: ' week2     /
15     'Year:  ' year;
16 run;
OUTPUT:
Day of Week: 4
Week1: 51
Week2: 52
Year:  2019
Listing 7-10

SAS WEEKDAY and WEEK Functions

In this example, the call to SAS WEEKDAY function is assigned to the dow variable and returns the value 4. 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 the WEEK 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 the call to the WEEK function followed by the optional 'v' descriptor. This descriptor uses Monday to represent the first day of the week and includes January 4 and the first Thursday of the year for week 1. If the first Monday of January is the second, third, or fourth, then these days are considered belonging to the week of the preceding year.

While the WEEKDAY functions for both Python and SAS are useful, they are often more meaningful if the returned integer values are mapped to names for the week day. In the case of Python, we introduce the calendar module. Like the date module, the calendar module uses January 1, 0001, as day 1 and provides calendar date handling functions and enables formatting. By default, the firstweekday() function is returns zero (0) and maps to Monday.

Begin by considering how the calendar.day_name attribute works. Listing 7-11 illustrates the calendar.day_name as a Python list which can be interrogated by supplying integer values between 0 and 6 for the index.
>>> import calendar
>>> list(calendar.day_name)
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
>>>
>>> calendar.day_name[6]
'Sunday'
Listing 7-11

Calendar.day_name Attribute

In this example, we supply an index value of 6 in order to return the string ‘Sunday’.

Now that we have the calendar module available, we can use it in a manner similar to the way SAS formats map values into other representations.

Listing 7-12 illustrates this concept. The dow2 object is assigned the integer value returned by calling the calendar.weekday() function for December 25, 2019.
>>> import calendar
>>>
>>> nl   = ' '
>>>
>>> dow2 = calendar.weekday(2019, 12, 25)
>>> nod2 = calendar.day_name[dow2]
>>>
>>> print(nl, 'Day of Week:' , dow2,
...       nl, 'Name of Day:' , nod2)
 Day of Week: 2
 Name of Day: Wednesday
Listing 7-12

Python Return Week Day Name

The dow2 object is used as the index to the calendar.day_name, returning the string ‘Wednesday’. Recall that Python uses a zero-based index. In this case, 0 maps to Monday.

The analog SAS example is shown in Listing 7-13.
4 data _null_;
5    d2   = '25Dec2019'd;
6    dow2 = weekday(d2);
7    nod2 = put(d2, weekdate9.);
8
9  put 'Day of Week: ' dow2 /
10     'Name of Day: ' nod2;
11 run;
OUTPUT:
Day of Week: 4
Name of Day: Wednesday
Listing 7-13

SAS Return Week Day Names

In this example, the d2 variable is a date variable and supplied as the parameter to the WEEKDAY function returning the value 4. The WEEKDAY function returns an integer value where one (1) is Sunday, two (2) is Monday, and so on. The PUT function creates the nod2 variable by assigning the dow2 variable value of 4 as the first parameter along with the SAS-supplied WEEKDATE9. format as the second parameter. The nod2 variable holds the value ‘Wednesday’.

The default Python weekday() function is a 0-based index mapped to Monday as the start of the week, while the SAS WEEKDAY function is a 1-based index mapped to Sunday as the start of the week.

Another common task with date arithmetic is calculating dates based on a duration. The Python replace() method offers a way to substitute the year, month, and day argument for a date object and return a modified date.

Consider Listing 7-14. In this example, we shift an arbitrary date back by six days.
>>> from datetime import datetime, date
>>> d1 = date(2018, 12, 31)
>>> if d1 == date(2018, 12, 31):
...     d2 = d1.replace(day=25)
...
>>> print(' ', 'Before replace():', d1,
...       ' ', 'After replace(): ', d2)
 Before replace(): 2018-12-31
 After replace():  2018-12-25
Listing 7-14

Python Date replace() Method

The replace() function replaces the day value of 31 with the new value 25, in effect shifting the d1 date object value back by six (6) days. The replace() method is useful for basic date arithmetic such as counting days to a future event.

Consider Listing 7-15. This example calculates the number of days from today’s date to a future date. It also illustrates conditional logic for handling cases when the future date falls in the current year, or when the future date falls into the succeeding year.
>>> 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'>
Listing 7-15

Python Count Days Until Birthday

In this example, the first argument to the date() constructor is the today.year argument which returns the current year followed by the integer 1 to represent January, then followed by the integer 24 to represent the 24th day. The if condition tests whether the input birthdate occurs during the current year.

If the birthday has already occurred in the current year, the if condition is False and not executed. If the birthday has not occurred in the current year, then the condition is True, and the replace() method is called to increment the year argument to year + 1.

The statement
days_until = abs(birth_day - today)

calculates the number of days between the input birthdate and today’s date.

The same logic is illustrated with SAS in Listing 7-16.
4    data _null_;
5
6    today = today();
7
8    birth_day = '24Jan19'd;
9
10   if birth_day < today then do;
11      next_birth_day = intnx('year', birth_day, 1, 'sameday');
12      days_until = abs(next_birth_day - today);
13      put 'Next birthday is: ' next_birth_day yymmdd10.;
14   end;
15
16   else do;
17      next_birth_day = birth_day;
18      put 'Next birthday is: ' next_birth_day yymmdd10.;
19      days_until = abs(next_birth_day - today);
20   end;
21
22      put days_until ' days';
23   run;
Next birthday is: 2020-01-24
240  days
Listing 7-16

SAS Count Days Until Birthday

The IF-THEN/DO-END block tests whether the input birthdate has occurred during the current year. If this evaluates true, we increment the birthday year by 1. This code block is like the replace() function in Listing 7-15. In SAS, incrementing or decrementing date values is accomplished using the INTNX function . In this case, the syntax
next_birth_day = intnx('year', birth_day, 1, 'sameday');
assigns to the next_birth_day variable a date constant returned from INTNX function with four parameters; they are
  1. 1.

    year’ is the interval value, that is, increment the date value by year increments.

     
  2. 2.

    birth_day designates the start date.

     
  3. 3.

    1 is the integer value to increment the start date, in this case, 1 year, since ‘year’ is the interval value. This parameter may be a negative value for decrementing from the start date.

     
  4. 4.

    SAMEDAY’ which is an optional alignment parameter.

     

With the next_birth_day calculated in this section of the code, the number of days remaining can be calculated and assigned to the days_until variable.

In the other case, when the input birthdate is greater than the current date (the false condition for the IF-THEN/DO-END), the ELSE/DO-END condition is executed to calculate the days remaining.

Shifting Dates

Another useful 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 fromordinal() method returns the integer value from a Python date object. Both these methods are illustrated in Listing 7-17.
>>> from datetime import datetime, date
>>>
>>> nl     = ' '
>>>
>>> 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'>
Listing 7-17

Python Date Shifted 1,001 Days

In this example, the date() constructor is called assigning the d1 datetime object a value of January 1, 2019. In order to determine the calendar date 1,001 days hence, the d1 datetime object calls the toordinal() method returning the number of days from the ordinal start date of January 1, 0001, and the date value for the d1 object assigning this value to the to_ord object. The to_ord object is then incremented by 1,001, and assigned to the inc object. Finally, the fromordinal() function is called with the inc object as an argument returning the incremented date to the d2 object.

The analog SAS program logic uses the INTNX function which increments date, time, or datetime values by a given interval and returns the incremented or decremented date, time, or datetime. See Listing 7-18.
4 data _null_;
5    d1 = '01Jan2019'd;
6    d2 = intnx('day', d1, 1001);
7 put 'Original Date: ' d1 yymmdd10. /
8     'Shifted Date:  ' d2 yymmdd10.;
9 run;
OUTPUT:
Original Date: 2019-01-01
Shifted Date:  2021-09-28
Listing 7-18

SAS Date Shifted 1,001 Days

Date Formatting

Now let’s examine formatting for dates and times. Both SAS and Python have many date formats to suit most requirements. Python uses two methods for formatting datetime values, strftime() and strptime().

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. Using the strftime() method to format a Python datetime object to control appearances is analogous to the way a SAS or user-defined format controls the appearances for displaying SAS datetime constants. The strftime() method also 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 result from calling the strftime() method is a Python date-, time-, or datetime-formatted string.

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 result from calling the strptime() method is a Python datetime object.

A mnemonic for remembering the difference in their behaviors is
  • strftime() is like string-format-time.

  • strptime() is like string-parse-time.

Table 7-1 displays the common format directives used for controlling the output to display Python datetime values.
Table 7-1

Common Python Datetime Format Directives 1

Directive

Meaning

Example

%a

Weekday abbreviated name

Sun, Mon, etc.

%A

Weekday full name

Sunday, Monday, etc.

%w

Weekday as a decimal, where 0 is Sunday

0, 1, …, 6

%b

Month abbreviated name

Jan, Feb, … Dec

%B

Month full name

January, February, etc.

%d

Day of month, zero-padded decimal

01, 02, …, 31

%Y

Year with century, zero-padded decimal

00, 01, …, 99

%j

Day of year, zero-padded decimal

001, 002, …, 366

%H

Hour, 24 hours as zero-padded decimal

00, 01, …, 23

%I

Hour, 12 hours as zero-padded decimal

01, 02, …, 12

%p

AM or PM

AM, PM

%M

Minute as zero-padded decimal

00, 01, …, 59

%S

Second as zero-padded decimal

00, 01, …, 59

%c

Date and time

Tue Aug 16 21:30:00 1988

Seeing working examples helps explain these concepts further. Start with Listing 7-19. The example begins by calling the date() constructor to assign the date January 24, 2019, to the dt datetime object.
>>> from datetime import date, time, datetime
>>>
>>> nl         = ' '
>>> 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'
Listing 7-19

Calling strftime() and strptime() Methods

The syntax
dt_str = dt.strftime('%d%B%Y')

creates the dt_str string object by assigning it the dt datetime value using the format directive %d%B%Y indicating the string object will be displayed as 24January2019. In SAS, converting a datetime object to a string is analogous to using the PUT function, along with an associated format to convert a numeric variable to a character variable.

The syntax which is part of the print() function
dt_frm_str.strftime('%A, %B %dth, %Y')
displays the dt_frm_str string object as
 Thursday, January 24th, 2019
We define fmt = '%A, %B %dth, %Y' which is used as an argument to the strftime() function
dt_frm_str.strftime(fmt)

In SAS, this is analogous to assigning a format to a variable to control its appearance. Also notice how arbitrary characters can be included as part of the format directive. In this case, %dth, uses the %d directive to display the day of the month as a zero-padded decimal followed by the characters th, to form the characters 24th,.

When converting Python datetime objects to strings using the strftime() method , the format directives behave similarly to SAS formats. And when converting strings to datetime objects using the strptime() method , the format directives behave similarly to SAS informats.

Dates to Strings

Consider Listing 7-20 which calls the strftime() method to convert Python date objects to strings.
>>> from datetime import date, time, datetime, timedelta
>>>
>>> nl      = ' '
>>> 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'>
Listing 7-20

Python Date Object to Strings

There are multiple calls to the strftime() method to illustrate formatting a string returned from datetime object using the pattern
dt_str = date.strftime(dN, format_directive)

The d3 datetime object is created by calling the date() constructor to set its date to December 25, 2019. Subsequently, three datetime-formatted strings are returned by calling the strftime() method with differing format directives. The format directive is given as the second argument to the strftime() method call and is composed of directives listed in Table 7-1. While these examples use a hyphen (-) as the field separator, you may use any separator such as a percent sign (%), slashes (/), colons (:), blanks (ASCII 32), and so on as well as including any arbitrary characters. Format directives are enclosed in quotes.

The analog SAS program is illustrated in Listing 7-21. The d3 variable is assigned a date constant and the VTYPE function returns N to indicate this variable is a numeric. With the d3 variable as the first parameter to the PUT function calls, the program illustrates different SAS formats to render different output styles similar to the Python format directives used in Listing 7-20.
4 data _null_;
5 length tmp_st $ 16;
6    d3        = '25Dec2019'd;
7    date_type = vtype(d3);
8
9    dt_str3 = put(d3,yymmdd8.);
10   ty_str3 = vtype(dt_str3);
11
12   dt_str4 = put(d3,yymmdd10.);
13   ty_str4 = vtype(dt_str4);
14
15   tmp_st  = strip(put(d3,worddatx.));
16   dt_str5 = translate(tmp_st,'-',' ');
17   ty_str5 = vtype(dt_str3);
18
19 put 'Date Variable d3:  ' d3 date9.  /
20     'd3 Date Type:      ' date_type  //
21      'dt_str3:           ' dt_str3   /
22      'dt_str3 Data Type: ' ty_str3   //
23      'd2_str4:           ' dt_str4   /
24      'dt_str4 Data Type: ' ty_str4   //
25      'dt_str5:           ' dt_str5   /
26   'dt_str5 Data Type: ' ty_str5;
27 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
Listing 7-21

SAS Dates to Character Variable

The PUT function is called to convert a numeric variable to a character variable. The PUT function returns a value using the format specified in the function call, in this case, the SAS-supplied YYMMDD8., YYMMDD10., and WORDDATX. formats. The VTYPE function returns the variable type, C for character and N for numeric.

Listing 7-22 illustrates calling the strftime() together with the format() function to dynamically format a string.
>> 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'
Listing 7-22

String Formatting with strftime() Method

The first positional parameter, {0}, to the format() function uses the directive
date.strftime(d4, '%j')
to return the day of the year as a decimal number. The second positional parameter, {1}, uses the directive
date.strftime(d4, '%A')

returning the week day full name.

Strings to Dates

Now let’s do the reverse process and convert a Python string object to a datetime object. The strptime() method is used to return a datetime object from a string. This feature is illustrated in Listing 7-23.
>>> from datetime import datetime, date
>>>
>>> nl     = ' '
>>> 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'>
Listing 7-23

Python Strings to Date Object

The dt_str object is assigned the string '2019-01-01'. The d5 datetime object is assigned the results from calling
d5 = datetime.strptime(dt_str, '%Y-%m-%d')

where the first argument is the dt_str object and the second argument is the directive matching this string.

Recall the strptime() method returns a datetime object, even though we are supplying a date string and the associated directive. As the name suggests, a datetime object holds a value for both date and time and is discussed later in this chapter. Since we do not have a time portion defined, the resulting d5 datetime object is set to midnight.

Listing 7-24 is the analog to Listing 7-23 illustrating the conversion of a character variable used for strings representing date to a SAS date constant.
4 data _null_;
5    dt_str      = '2019-01-01';
6    d5          = input(dt_str,yymmdd10.);
7    dt_str_type = vtype(dt_str);
8    d5_type     = vtype(d5);
9
10 put 'Original String: ' dt_str /
11     'Data Type: ' dt_str_type  //
12     'Date is: ' d5 yymmdd10.   /
13     'Data Type: ' d5_type ;
14  run;
OUTPUT:
Original String: 2019-01-01
Data Type: C
Date is: 2019-01-01
Data Type: N
Listing 7-24

SAS Character Variable to Date

In contrast to Listing 7-21, this example uses the INPUT function to map a date string assigned to the dt_str variable as a date constant. The VTYPE function returns the data type for the d5 and dt_str variables.

Time Object

For Python, a time object represents the local time of day, independent of any particular day, and is subject to changes based on the tzinfo object . The tzinfo object is discussed later in this chapter in the section on time zone. The time module uses an epoch start, or offset, from midnight on January 1, 1970. This fact is illustrated in Listing 7-25. Keep in mind that the datetime module also has methods and functions for handling time. We will see additional Python time handling techniques in the datetime object section later in this chapter.

Returning the Python epoch start is illustrated in Listing 7-25.
>>> 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)
Listing 7-25

Python Time Epoch

The gmtime() function converts a time value expressed as the number of seconds, in this case, zero, to the 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 which can be accessed by an index value and attribute name. Think of the struct_time object as a data structure used by the time module to provide access to time-related components analogous to a SAS time value.

Once a struct_time object is created, it exposes an index and attributes to return time-related components similar to SAS functions used to return time-related components from a SAS time constant.

Table 7-2 displays the index and attribute names used to return constituent components of a time value.
Table 7-2

Python struct_time Object 2

Index

Attribute

Values

0

tm_year

For example, 2018

1

tm_mon

1 for Jan, 2 for Feb, etc. 1–12

2

tm_mday

1 for first, 2 for second, etc. 1–31

3

tm_hour

0 for midnight, 1, 2, etc. 0–23

4

tm_min

0, 1, 2, …, 59

5

tm_sec

0, 1, 2, …, 59

6

tm_wday

0 for Monday, 1 for Tuesday, …, 6 for Sunday

7

tm_yday

1, 2,…, 366

Notice how the struct_time object returns a year, month, and date value. Python time values have a resolution to the microsecond, or one millionth of a second, whereas SAS time values have a resolution of 1 second. Also notice that tm_mon, tm_mday, and tm_day are one-based (1) indexes rather than the traditional zero-based (0) index.

Listing 7-26 illustrates returning constituent components composing the time value assigned to the t time object. This example calls the gmtime() function with a timestamp value of zero, like Listing 7-25.
>>> import time
>>>
>>> nl = ' '
>>> 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
Listing 7-26

Python struct_time Object

Except for the year, attribute names are used to return the values from the struct_time object. The year value is returned using an index value of zero (0).

Listing 7-27 is the analog to the Python example in Listing 7-26. This example 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.
4  data _null_;
5    t  = '00:00:00't;
6   hr  = hour(t);
7   mn  = minute(t);
8   sc  = second(t);
9   mo  = month(t);
10  dy  = day(t);
11  yr  = year(t);
12
13 put "'00:00:00't is: " t datetime32. /
14     'Hour:         ' hr /
15     'Minute:       ' mn /
16     'Second:       ' sc /
17     'Day of Month: ' mo /
18     'Day of Week:  ' dy /
19     'Year:         ' yr;
20  run;
OUTPUT:
'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
Listing 7-27

SAS Time Example

In this example, the SAS datetime offset of midnight, January 1, 1960, is assigned to the t variable. This variable is used as the argument to several SAS date and time functions to return constituent components for this time constant.

Time of Day

In some cases, the analysis we are performing needs to consider 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 make calls to the underlying OS to return time of day from the processor’s clock. Later in this chapter, we will explore how to handle variations in reported time influenced by time zone considerations.

For now, let’s consider the basics for obtaining the current time as shown in Listing 7-28. Recall that in Listing 7-25 we use the gmtime() function to create a struct_time object by supplying the number of seconds from the Python time offset, which is zero (0) in that case. In this example, we call the time.time() function without an argument to return the local time.

In Python, calling the time.time() function returns the current time of day as a timestamp, that is, the number of seconds from the Python time offset. In order to return the local time in a human-readable form, we need to embed the time.time() function call inside the localtime() function call. Observe in Listing 7-28 how the t_stamp object is assigned the value from the time.time() function and returns a float, which is the timestamp value. In order to create the struct_time object for time, we use the localtime() function with time.time() function as the argument.
>>> import time
>>>
>>> nl      = ' '
>>> 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'>
Listing 7-28

Python Return Time of Day

Specifically, the syntax
now = time.localtime(time.time())

creates the now object by calling the localtime() function using the time.time() as an argument, which returns today’s timestamp to the localtime() function. The result is the struct.time object assigned to the now object.

The syntax
dsp_now = time.asctime(now)

converts the time.struct 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 into a string.

Listing 7-29 is the analog to Listing 7-28. It uses the SAS DATETIME function to return the current date and time.
4  data _null_;
5
6     now = datetime();
7     dow = put(datepart(now), weekdate17.);
8     tm  = put(timepart(now), time9.);
9     yr  = year(datepart(now));
10    bl  = ' ';
11    all = cat(tranwrd(dow,',',' '), tm, bl, yr);
12
13 put 'all Variable: ' all;
14 run;
all Variable: Tue  Jan 15  2019 13:50:18 2019
Listing 7-29

SAS Return Time of Day

The now variable is a time constant with a value from today’s date and time. The PUT function converts this numeric variable to a character variable, in this case, using the DATEPART, TIMEPART, and YEAR functions returning constitute datetime components. The TRANWRD function strips the comma (,) following the week day name.

Time Formatting

Formatting time values follows the same pattern discussed earlier in the chapter for formatting date values using the same format directives used for date and datetime objects. Additionally, 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 to control its appearances. The result from calling the strptime() function is a datetime object whose output is controlled by the corresponding format directive.

It is often the case that time and datetime values need conversion to strings or we have strings representing time and datetime values which need conversion to time or datetime objects and variables. The next four examples illustrate these conversions along with formatting time and datetime examples.

Times to Strings

We begin with Listing 7-30 calling the strftime() function to convert a Python time object, specifically converting the struct_time object components to a string.
>>> import time
>>>
>>> nl  = ' '
>>>
>>> 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
Listing 7-30

Python Time Object to Strings

As we saw earlier, we acquire the local time of day by calling time.time() to return a timestamp which is then passed to the localtime() function to return a struct_time object. This is accomplished with the syntax
now = time.localtime(time.time())
The n1 object calls the asctime() function , which returns a string with a default format as
Tue Jan 15 15:48:28 2019

without the need to supply a format directive.

Objects n2, n3, and n4 display the same datetime with different format directives. Format directives are supplied as a single argument composed of the directives listed in Table 7-1, in this case, using different separators such as hyphens (-), slashes (/), and colons (:) and any arbitrary characters.

The corresponding SAS program is illustrated in Listing 7-31. The results are the creation of the n1, n2, n3, and n4 variables whose values are strings matching the n1, n2, n3, and n4 Python string objects in Listing 7-30.
5 data _null_;
6
7    now = datetime();
8    dt  = put(datepart(now), weekdate17.);
9    tm  = put(timepart(now), time9.);
10   yr  = year(datepart(now));
11
12   bl  = ' ';
13   n1  = cat(tranwrd(dt,',',' '), tm, bl, yr);
14   n2  = cat(put(datepart(now), yymmdds10.), put(timepart(now), time6.));
15   n3  = put(timepart(now), timeampm11.);
16   n4  = cat(put(datepart(now), yymmddd10.), put(timepart(now), time6.), bl, tzonename());
17
18 put 'Variable n1: ' n1 /
19     'Variable n2: ' n2 /
20     'Variable n3: ' n3 /
21     'Variable n4: ' n4;
22 run;
OUTPUT:
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
Listing 7-31

SAS Time Constants to Strings

This example calls the DATETIME function to return the current date and time similar to the Python time() function. The Python time() function returns the struct_time object, whereas the SAS TIME function returns a time constant, ‘12:34:56t, representing the number of seconds from midnight.

In order to replicate the results in Listing 7-30, this example uses different SAS functions such as DATEPART and TIMEPART to extract the constituent elements from the current date and time, respectively. The TZONENAME function is a relatively new function introduced with release 9.4, returning the time zone name, assuming this option is set. By default, this option is not set and is set using the OPTIONS statement. We provide further details for handling time zone later in this chapter.

Strings to Time

When you have strings representing date or time values, use the strptime() function to convert them to a Python time object. This feature is illustrated in Listing 7-32. Strictly speaking, this example converts a string representing a time value to the struct_time object.
>>> import time
>>>
>>> nl  = ' '
>>> 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'>
Listing 7-32

Python Strings to Time Object

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 called t. The attributes tm_hour, tm_min, and tm_sec attached to the t object return integer values representing the hour, minute, and seconds, respectively.

The analog SAS program is shown in Listing 7-33.
4 data _null_;
5
6    t_str = '12:34:56';
7    t     = input(t_str,time9.);
8
9    hr    = hour(t);
10   hr_t  = vtype(hr);
11
12   min   = minute(t);
13   min_t = vtype(min);
14
15   sec   = second(t);
16   sec_t = vtype(sec);
17
18 put 't Variable:    ' t timeampm11. //
19     'hr Variable:   ' hr            /
20     'hr Data Type:  ' hr_t          //
21     'min Variable:  ' min           /
22     'min Data Type: ' min_t         //
23     'sec Variable:  ' sec           /
24     'sec Data Type: ' sec_t;
25 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
Listing 7-33

SAS Character Variable to Time Constant

In this example, 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, from the t variable.

Datetime Object

The Python datetime module provides a set of functions and methods for creating, manipulating, and formatting datetime objects. These methods and functions behave similarly to those from the date and time modules discussed earlier. In contrast to the date and time modules, here we discuss the datetime object to facilitate the handling of date and time values as a single object. We should point out that the Python examples in the “Date Object” section of this chapter import the date module and the examples in the “Time Object” section import the time module. So naturally, in this section we import the datetime module.

Like a Python date object, the datetime object assumes the current Gregorian calendar extended in both directions, and like the time object, a datetime object assumes there are exactly 3600*24 seconds in every day.

Likewise, while 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

We begin with constructing a datetime object. The datetime() constructor method requires the year, month, and day arguments with the remaining arguments optional.

Listing 7-34 illustrates creating a datetime object with different constructor methods, each returning a datetime object. This example illustrates datetime.datetime(), datetime.datetime.now(), and datetime.datetime.utcnow() constructor methods.
>>> import datetime as dt
>>>
>>> nl       = ' '
>>> 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'>
Listing 7-34

Python Datetime Constructor Methods

The datetime.datetime() constructor method accepts a tuple of values conforming to the ranges described earlier. The datetime.utc() constructor method returns the current date based on the UTC time zone, while the datetime.now() 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() constructor method. Each of these methods returns a datetime object.

Likewise, we can construct a Series of SAS datetime constants. Listing 7-35 is the analog to Listing 7-34. The first portion of this example creates the user-defined py_fmt. format displaying SAS datetime constants in the same manner as the default Python datetime objects.

The PROC FORMAT PICTURE formatting directives3 mostly follow the Python formatting directives shown in Table 7-1. The parameter to the PICTURE statement
(datatype=datetime)
is required indicating the format PICTURE is applied to a datetime variable. With the exception of the %Y for the year format directive, the remaining format directives have a zero (0) as part of the directive to left-pad the value when any of the datetime values return a single digit. This is the same behavior as the default Python format directives.
4 proc format;
5    picture py_fmt (default=20)
6     low - high = '%Y-%0m-%0d %0I:%0M:%0s' (datatype=datetime);
7
NOTE: Format PY_FMT has been output.
8 run;
9
10 data _null_;
11
12    dt_str     = '24Oct2018:01:23:45';
13    dt_obj     = input(dt_str, datetime20.);
14    dt_obj_ty  = vtype(dt_obj);
15
16    dt_now1    = datetime();
17    dt_now1_ty = vtype(dt_now1);
18
19    dt_now2     = tzones2u(dt_now1);
20    dt_now2_ty  = vtype(dt_now2);;
21
22 put 'dt_obj Variable: '   dt_obj py_fmt.  /
23    'dt_obj Data Type: '  dt_obj_ty       //
24    'dt_now1 Variable: '  dt_now1 py_fmt. /
25    'dt_now1 Data Type: ' dt_now1_ty      //
26    'dt_now2 Variable: '  dt_now2 py_fmt. /
27    'dt_now2 Data Type: ' dt_now2_ty;
28 run;
OUTPUT:
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
Listing 7-35

Constructing SAS Datetime Constants

The DATETIME function is analogous to the Python datetime.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. We cover time zone further in the “Time zone Object” section later in this chapter.

Combining Times and Dates

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. This feature is shown in Listing 7-36.
>>> from datetime import datetime, date, time
>>>
>>> nl      = ' '
>>> 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'>
Listing 7-36

combine() Method for Python Dates and Times

And as one would expect, the data type returned for the dt_comb object is datetime.

With SAS, use the DHMS function to combine a date and time variable. This logic is illustrated in Listing 7-37.
4 data _null_;
5    time     = '12:34:56't;
6    date     = '31May2019'd;
7    new_dt   = dhms(date,12,34,56);
8
9    new_dt_ty = vtype(new_dt);
10
11    put 'new_dt Variable:  ' new_dt py_fmt. /
12        'new_dt Data Type: ' new_dt_ty;
13 run;
new_dt Variable:   2019-05-31 12:34:56
new_dt Data Type: N
Listing 7-37

Combining SAS Date and Time Constants

Calling the DHMS function returns a numeric value that represents a SAS datetime value. The py_fmt. format is created in Listing 7-35.

Now let’s turn our attention to extracting constituent components from a datetime object . Both Python and SAS provide methods to extract datetime components to returning these values as integers and strings.

Returning Datetime Components

We begin by calling Python datetime attributes to return date and time components as integers. See Listing 7-38. In this example, the date() and time() methods are attached to the dt_obj object returning date and time objects, respectively. Python datetime objects have the year, month, day, hour, minute, and second attributes to return integer values.
>>> import datetime as dt
>>>
>>> nl = ' '
>>> 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
Listing 7-38

Python Attributes Returning Datetime Components

Not shown in this example are the microsecond and tzinfo attributes which in this case return None.

Listing 7-39 performs the same operation as Listing 7-38. The DATEPART and TIMEPART functions return the date and time as date and time constants, respectively.
4 data _null_;
5
6 dt_obj = '09Jan2019:19:34:56'dt;
7
8     date   = datepart(dt_obj);
9     time   = timepart(dt_obj);
10
11    year   = year(datepart(dt_obj));
12    month  = month(datepart(dt_obj));
13    day    =   day(datepart(dt_obj));
14
15    hour   = hour(dt_obj);
16    minute = minute(dt_obj);
17    second = second(dt_obj);
18
19 put 'Date:    ' date yymmdd10.  /
20     'Time:    ' time time8.     /
21     'Year:    ' year            /
22     'Month:   ' month           /
23     'Day:     ' day             /
24     'Hour:    ' hour            /
25     'Minute:  ' minute          /
26     'Seconds: ' second;
27 run;
OUTPUT:
Date:    2019-01-09
Time:    19:34:56
Year:    2019
Month:   1
Day:     9
Hour:    19
Minute:  34
Seconds: 56
Listing 7-39

SAS Functions Returning Datetime Components

All of the variables in this example are numeric.

Strings to Datetimes

The next two examples illustrate converting Python and SAS strings to datetimes. Converting a Python string representing a datetime value to a datetime object follows the same pattern we saw earlier for converting date and time string objects by calling the strptime() function . Start with Listing 7-40. The strptime() function accepts two parameters: the first is the input string representing a datetime value followed by the second parameter, the format directive matching the input string.
>>> import datetime as dt
>>>
>>> nl = ' '
>>>
>>> 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'>
Listing 7-40

Python Strings to Datetime Objects

Converting a SAS character variable to a datetime constant is similar to the earlier examples in this chapter for converting character variables to date and time constants. Consider Listing 7-41 which illustrates this conversion. In 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. 1.

    Create a user-defined informat with PROC FORMAT.

     
  2. 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. 3.

    Call PROC FORMAT to build the user-defined informat by loading the CNTLIN= input dataset.

     
  4. 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.

     
4 proc format;
5    picture py_infmt (default=20)
6    low - high = '%B %d, %Y %I:%0M%p' (datatype=datetime);
NOTE: Format PY_INFMT has been output.
7 run;
8 data py_infmt;
9    retain fmtname "py_infmt"
10          type "I";
11    do label = '04Aug2001:00:00'dt to
12               '05Aug2001:00:00'dt by 60;
13      start  = put(label, py_infmt32.);
14      start  = trim(left (start));
15     output;
16 end;
17 run;
NOTE: The dataset WORK.PY_INFMT has 1441 observations and 4 variables.
18 proc format cntlin = py_infmt;
NOTE: Informat PY_INFMT has been output.
19 run;
NOTE: There were 1441 observations read from the dataset WORK.PY_INFMT.
20 data _null_;
21    str        = 'August 4, 2001 1:23PM';
22    dt_obj     = input(str, py_infmt32.);
23    dt_obj_ty  = vtype(dt_obj);
24    tm         = timepart(dt_obj);
25    tm_ty      = vtype(tm);
26    yr         = year(datepart(dt_obj));
27    yr_ty      = vtype(yr);
28
29   put  'dt_obj Variable: '   dt_obj py_infmt32. /
30        'dt_obj Data Type: '  dt_obj_ty          //
31        'yr Variable: '       yr                 /
32        'yr Data Type: '      yr_ty              //
33        'tm Variable: '       tm time8.          /
34        'tm Data Type: '      tm_ty;
35   run;
OUTPUT:
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
Listing 7-41

Converting SAS Character Variable to Datetime Variable

The format directive '%B %d, %Y %I:%0M%p' on the PICTURE statement matches the input character variable value 'August 4, 2001 1:23PM'. The SAS Data Step creating the py_infmt dataset contains the minimum variables for defining a CNTLIN= dataset needed by PROC FORMAT, with the type, label, and start variables. In order to be recognized as a cntlin= dataset, these names for variables are required. SAS datetime values have a 1-second level of granularity, and since the user-defined informat deals with a minute interval, the DO loop to generate the datetime labels is divided by 60.

In the _null_ Data Step, the syntax
dt_obj = input(str, py_infmt32.);

assigns the SAS datetime constant to the dt_obj variable by calling the INPUT function with the user-defined py_infmt32. informat.

Datetimes to Strings

Now let’s 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 . Listing 7-42 illustrates the use of the Python formatted string literals, or f-string for short. An f-string contains replacement fields designated by curly braces { } and is composed of expressions evaluated at runtime.
>>> import datetime as dt
>>>
>>> nl     = ' '
>>> 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
Listing 7-42

Python Datetime Object to Strings

A format specifier is appended following the colon (:) and while the format specifiers can be a range of valid format specifiers, since we are dealing with datetime, we illustrate those in Table 7-1.

In the case of SAS, extracting the datetime components uses the same functions illustrated in Listing 7-43 along with the PUT function to convert the returned numeric values into character variables.
4 data _null_;
5
6 dt_obj      = '15Jul2019:02:34:56'dt;
7
8  wkdy_str   = put(datepart(dt_obj), downame9.);
9  mnname_str = put(datepart(dt_obj), monname.);
10
11  day_str   = put(day(datepart(dt_obj)), 8.);
12  yr_str    = put(year(datepart(dt_obj)), 8.);
13
14  hr_str    = cat('0',left(put(hour(dt_obj), 8.)));
15  mn_str    = put(minute(dt_obj), 8.);
16  sec_str   = put(second(dt_obj), 8.);
17
18  put 'Weekday: ' wkdy_str   /
19      'Month:   ' mnname_str /
20      'Day:     ' day_str    /
21      'Year:    ' yr_str     /
22      'Hours:   ' hr_str     /
23      'Minutes: ' mn_str     /
24      'Seconds: ' sec_str    /;
25  run;
OUTPUT:
Weekday: Monday
Month:   July
Day:     15
Year:    2019
Hours:   02
Minutes: 34
Seconds: 56
Listing 7-43

SAS Datetime to Character Variables

In order to render the output identical to the Python example in Listing 7-42, the CAT function is called to left-pad the value for the hr_str variable with a zero (0).

Timedelta Object

The Python Timedelta object, as the name suggests, represents a duration between two date or time objects with a granularity of 1 microsecond. A rough corollary in SAS is the INTCK and INTNX functions . The SAS 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 object 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 objects support certain addition and subtraction operations using date and datetime objects.

Let’s see some examples starting with Listing 7-44. It begins by assigning the now object the current local datetime. The timedelta() method is called to shift backward 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        = ' '
>>> 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
Listing 7-44

Basic Timedelta Arithmetic

The objects dy_ago1, dy_ago2, wk_ago, and yr_fm_now are datetime objects shifted in time by the different Timedelta arguments. new_td is simply a Timedelta object.

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

    Obtain the target date.

     
  2. 2.

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

     
  3. 3.

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

     
This approach is illustrated in Listing 7-45.
>>> import datetime as dt
>>>
>>> nl    = ' '
>>> 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
Listing 7-45

Python First and Last Day of Month

The fd_mn object is defined as the first day of the month by replacing the current day date ordinal with the value of one (1) using the replace() function . Finding the last day of the month involves two steps: first, finding the first day of the succeeding month of the input date and, second, subtracting one day from this date. For step 1, we find the “next” month using the 28th as the logical last day of every month in order to handle the month of February. The syntax
nxt_mn = date.replace(day=28) + datetime.timedelta(days=4)
replaces the actual day date ordinal with 28 and adds a Timedelta of 4 days ensuring the nxt_mn datetime object will always contain a date value for the “next” month. In step 2, we return the day date ordinal from the “next” month using the nxt_nm.day() attribute. This returned ordinal is the number of days we decrement from the nxt_nm datetime object using the syntax
 ld_mn = nxt_mn - datetime.timedelta(nxt_mn.day)

creating the ld_mn datetime object holding the last day of the “current” month.

The SAS INTNX function is analogous to the Python timedelta() function able to use a datetime constant and shift values by incrementing or decrementing by date or datetime intervals. This feature is illustrated in Listing 7-46. A DO/END loop is used to generate the date constants using a BY value of 31. It simply generates 12 date constants with each value falling into a separate month of the year.
4 data _null_;
5
6 put 'First Day of the Month   Last Day of the Month' /
7     '==============================================' /;
8 do date_idx = '01Jan2019'd to '31Dec2019'd by 31;
9    f_day_mo = intnx("month", date_idx, 0, 'Beginning');
10   l_day_mo = intnx("month", date_idx, 0, 'End');
11    put f_day_mo weekdate22.  l_day_mo weekdate22.;
12 end;
13
14 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
Listing 7-46

SAS First and Last Day of Month

The statement
f_day_mo = intnx("month", date_idx, 0, 'Beginning');
returns the first day of the month assigning this date constant to the f_day_mo variable by calling the INTNX function with four parameters where
  • "month" is a keyword to indicate the interval to be shifted between the start and end dates.

  • date_idx indicates the start-from date or datetime constant to be shifted.

  • 0 to indicate how many intervals to shift the start-from date or datetime constant. In this case, we are not shifting the date_idx date constant and instead finding the 'Beginning' of the interval, which is the beginning of the month.

  • 'Beginning' is an optional parameter used to control the position of the date or datetime constant within the interval, in this case, the beginning or first day of the month.

Similarly, the l_day_mo variable behaves in the same manner as the f_day_mo variable when calling the INTNX function with the exception of the last parameter being 'End' to find the last day of the month.

Of course, we can easily extend the logic for finding the first and last day of the month to finding the first and last business day of the month. This is illustrated in Listing 7-47 and Listing 7-49. Each of the four functions, when called, accepts two positional parameters, year and month.

We begin with the Python functions. Because we already have the Python program to find the first and last day of the month, they are easily converted into functions named first_day_of_month and last_day_of_month. See Listing 7-47.

Next, we 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, whose scope is local to each function definition. Its role inside the function is twofold. First, it receives the year and month input parameters when these functions are called setting an “anchor” date as the first day of the inputs for month and year and second is to manipulate the “anchor” date with datetime arithmetic to return the appropriate date.
import datetime as dt
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)
Listing 7-47

Python Functions for First, Last, First Biz, and Last Biz Day of Month

For the first_biz_day_of_month and last_biz_day_of_month functions , the weekday() function tests whether the ordinal day of the week returned is five (5) representing Saturday or six (6) representing Sunday.

When the weekday() function returns five, the local any_date datetime object is shifted forward to the following Monday by adding a Timedelta of 2 days. The same logic applies for Sunday, that is, when the ordinal 6 is returned from the weekday() function , the any_date object is shifted to the following Monday by adding a Timedelta of 1 day.

In the case of the last_biz_day_of_month() function , we can find the last day using the two-step process described in Listing 7-45.

The last_biz_day_of_month() function needs to be able to detect if the local any_date datetime object falls on a Saturday or Sunday and decrement accordingly. Finding the last business day of the month involves handling three conditions, the default condition, the Saturday condition, and the Sunday condition.
  1. 1.
    Default condition
    1. 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 .

       
    2. b.

      Add a Timedelta of 4 days to the local nxt_mn datetime object ensuring the month for this date value is the month succeeding the month of the “anchor” date. By replacing this day ordinal with 28 and adding 4 days, you now have the first day of the succeeding month. Subtracting 1 day returns the last day of the “anchor” month.

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

      Use of the weekday() function to test if the returned ordinal is 5. When True, decrement the local nxt_mn datetime object by 1 day to the preceding Friday.

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

      Use of the weekday() function to test if the returned ordinal is 6. When True, decrement the local nxt_mn datetime object by 2 days to the preceding Friday.

       
     
With these four functions defined, they can be called as illustrated in Listing 7-48. This example only works if the code defining these functions, in Listing 7-47, has already been executed in the current Python execution environment.
>>> nl    = ' '
>>> 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
Listing 7-48

Calling Functions for First, Last, Day of Month and Biz Day of Month

These four functions are each called with a year and month parameter, in this case, February 2020. These functions illustrate basic concepts but have no error handling logic for malformed input nor do they take into account instances where a business day falls on a holiday, for example, New Year’s Day.

Listing 7-49 extends the logic for Listing 7-46. In this case, we use 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.
4 data _null_;
5 put 'First Biz Day of Month   Last Biz Day of Month' /
6      '==============================================' /;
7 do date_idx  = '01Jan2019'd to '31Dec2019'd by 31;
8    bf_day_mo = intnx("month", date_idx, 0, 'Beginning');
9    bl_day_mo = intnx("month", date_idx, 0, 'End');
10
11    beg_day  = weekday(bf_day_mo);
12    end_day  = weekday(bl_day_mo);
13
14    /* If Sunday increment 1 day */
15    if beg_day = 1 then bf_day_mo + 1;
16       /* if Saturday increment 2 days */
17       else if beg_day = 7 then bf_day_mo + 2;
18
19    /* if Sunday decrement 2 days */
20    if end_day = 1 then bl_day_mo + (-2);
21      /* if Saturday decrement 1 */
22       else if end_day = 7 then bl_day_mo + (-1);
23
24    put bf_day_mo weekdate22.  bl_day_mo weekdate22. ;
25 end;
26 run;
OUTPUT:
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
Listing 7-49

SAS First and Last Business Day of Month

The beg_day variable returns the ordinal for week day representing the first of the month, and the end_day variable returns the ordinal for week day representing the last of the month.

The IF-THEN/ELSE block uses the beg_day and end_day variables to determine if the date falls on a Saturday. In the case where it is the beginning of the month, we add two (2) days to shift to the following Monday. And in the case where it is month end, we decrement by one (1) day to shift to the preceding Friday.

Similarly we need logic for detecting if a date falls on a Sunday. In the case where it is the beginning of the month, we add one (1) day to shift to the following Monday. And in the case where it is the month end, we decrement by two (2) days to shift to the preceding Friday.

Time zone Object

It turns out we can write an entire chapter or even an entire book on time zone related to time and datetime objects. Consider this challenge related to working with any datetime object; on November 4, 2018, clocks in the United States returned to Standard Time. In the US/Eastern time zone, the following events happened:
  • 01:00 EDT occurs.

  • One hour later, at what would have been 02:00 EDT, clocks are turned back to 01:00 EST.

As a result, every instant between 01:00 and 02:00 occurred twice that day. In that case, if you created a time object in the US/Eastern time zone using the standard datetime library, you were unable to distinguish between the time before and after this transition.

And this is just the beginning of the timeless challenges facing data analysis tasks having datetime values. Generally, time zone offsets occur at 1-hour boundaries from UTC, which is often true, except in cases like India Standard Time which is +5:30 hours UTC. In contrast, New York, in the United States, is -5:00 hours UTC, but only until March 10, 2019, when EST transitions to DST.

As the world around us becomes more instrumented with billions of devices emitting timestamped data needing to be analyzed, the requirement for handling datetime in a time zone “aware” manner becomes more acute.

Naïve and Aware Datetimes

The Python Standard Library for datetime has two types of date and datetime objects: “naïve” and “aware”. Up to this point, the Python date and datetime examples used in our examples are “naïve”, meaning it is up to the program logic to determine the context for what the date and datetime values represent. This is much the same way a calculated linear distance value can be rendered in miles, meters, or kilometers. The benefit of using “naïve” datetime types is the simplicity for manipulation and ease of understanding. The downside is they do not have enough information to deal with civil adjustments to times or other shifts in time like those described previously.

In this section we will touch on the Python timezone object as means for representing time and datetime objects with an offset from UTC. We also introduce the pytz module to provide consistent cross-time zone time and datetime handling. The pytz module augments working with “aware” time and datetime objects by exposing an interface to the tz database4 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 then converting to the local time zone for human-readable output. No Daylight Savings time occurs in UTC making it a useful time zone for performing datetime arithmetic, not to mention that nearly all scientific datetime measurements use UTC and not the local time zone.

A datetime object is “naïve” if its .tzinfo attribute returns None. The following examples illustrate the distinctions between “naïve” and “aware” datetime objects.

Begin by considering Listing 7-50. Even the simple request for time through a function call needs consideration. 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 sometimes determined by the physical location of the servers, which themselves are often located in different locations around the world. Calling the datetime utcnow() function to return the current datetime eliminates this uncertainty.
>>> import datetime as dt
>>>
>>> nl            = ' '
>>> 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
Listing 7-50

Python Naïve and Aware Datetime Objects

In this example, using the Python Standard Library, we make three requests for the current time assigning returned values to datetime objects. The first request calls the datetime.now() method returning the current local time to the “naïve” datetime object dt_local. In this case, the current local time comes from the US Eastern time zone, although the time zone is inferred, since we do not provide one when making the call. The tzinfo attribute value returned from the dt_local object confirms this is a “naïve” datetime by returning None.

The second request uses the datetime.utcnow() method call to return the current time from the UTC time zone and assign it to the dt_naive object. Interestingly, when we request the current time from the UTC time zone, we are returned a “naïve” datetime object. And because both the dt_local and dt_naive objects are “naïve” datetimes, this explains why the request for the time zone value using the (%Z) format directive returns an empty value when printing.

The third request illustrates returning the current UTC time zone and assigning the value to the “aware” dt_aware datetime object calling the datetime.now(dt.timezone.utc) method. In this case, the .tzinfo attribute returns UTC to indicate this datetime object is “aware”.

Using the Standard Library, we can convert a “naïve” datetime object to an “aware” datetime object, but only to the UTC time zone by calling the replace() method using the timezone.utc argument. The conversion of a “naïve” to an an “aware” datetime object is illustrated in Listing 7-51.

This example begins by converting the dt_str object to the dt_naive datetime object calling the strptime() method . Next, we call the replace() method to, in effect, replace the None value for the tzinfo attribute of the dt_obj_naive object to UTC and keep the remainder of the datetime object values and assigning results to the dt_obj_aware object.
>>> from datetime import datetime, timezone
>>>
>>> nl       = ' '
>>> 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
Listing 7-51

Converting Naïve to Aware Datetime Object

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

pytz Library

Clearly there are cases where we need to convert “naïve” objects to “aware” objects with time zone attributes other than UTC. And in order to do so, we need to utilize a third-party library such as the widely used pytz library.5 In Listing 7-52, we convert the datetime string 2019-01-24 12:34:56 having no time zone attribute to an “aware” datetime object for the US Eastern time zone. The pytz library provisions the timezone() function which is a method modifying the tzinfo attribute of a datetime object with a supplied time zone designation.
>>> from datetime import datetime
>>> from pytz import timezone
>>>
>>> nl       = ' '
>>> 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
Listing 7-52

Python Aware Datetime Object with timezone.localize

In this example, we create the “aware” datetime object from a datetime string. The syntax
 dt_est = timezone('US/Eastern').localize(dt_naive)

creates the dt_est “aware” datetime object by calling the pytz timezone() function which has the effect of setting this object’s tzinfo attribute to “US/Eastern”. The localize() method is chained to this call to convert the “naïve” dt_naive datetime object to an “aware” object. The localize() method takes into account the transition to Daylight Savings time.

Before we go further, the question naturally arising is where to find the list of valid time zone values. Fortunately, the pytz library provides data structures to return this information. One structure is the common_timezones which is returned as a Python list, illustrated in Listing 7-53. At the time of this writing, it returns 439 valid time zone values available to the timezone() function .
>>> import random
>>> from pytz import common_timezones
>>> print(' ', len(common_timezones), 'entries in the common_timezone list', ' ')
 439 entries in the common_timezone list
>>> print(' '.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
Listing 7-53

Random pytz Common Time zone

In this example, we return a random sample of ten time zone values from the common_timezones list. Another source of time zone values is the all_timezones list which is an exhaustive list of time zone available to the pytz timezone() function.

Yet another useful list is the country_timezones. By supplying an ISO 31666 two-letter country code, the time zone in use by the country is returned. Listing 7-54 illustrates returning the Swiss time zone from this list.
>>> import pytz
>>> print(' '.join(pytz.country_timezones['ch']))
Europe/Zurich
Listing 7-54

pytz country_timezones List

Even better is the country_timezones() function which accepts the ISO 3166 two-letter country code and returns the time zone in use by the country as illustrated in Listing 7-55.
>>> from pytz import country_timezones
>>>
>>> print(' '.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
Listing 7-55

pytz country_timezones() Function

Returning to the pytz timezone() function , let’s look at additional examples and pitfalls to avoid.

In Listing 7-51, we used the datetime replace() method to replace the tzinfo attribute with UTC returning a datetime “aware” object. So what happens when you use this approach with the pytz library? Listing 7-56 illustrates the pitfall of using the datetime replace() method with a time zone from the pytz library. Unfortunately, calling the datetime replace() function with a tzinfo argument from the pytz library does not work.
>>> from datetime import datetime, timedelta
>>> from pytz import timezone
>>> import pytz
>>>
>>> nl       = ' '
>>> 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
Listing 7-56

pytz Interaction with tzinfo Attribute

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 6 minutes. Obviously, if both times are the same times represented by different time zone, 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. Consider Listing 7-57.
>>> from datetime import datetime, timedelta
>>> from pytz import timezone
>>> import pytz
>>>
>>> nl       = ' '
>>> 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
Listing 7-57

Datetime astimezone() Conversion Function

If you need to do datetime manipulation using local datetimes, you must use the normalize() method in order to properly handle Daylight Savings and Standard Time transitions. Consider Listing 7-58 where the object dt_loc calls the localize() method to create a datetime object for 1:00 am, November 4, 2018, which is the US transition date and time from Daylight Savings to Standard Time.
>>> from datetime import datetime, timedelta
>>> from pytz import timezone
>>> import pytz
>>>
>>> nl        = ' '
>>> 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
Listing 7-58

Handling DST Transition

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. And by adding a Timedelta of 20 minutes to the before datetime object, it returns a datetime for Eastern Standard Time.

Nonetheless, the preferred way of handling datetime manipulation is to first convert datetimes to the UTC time zone, perform the manipulation, and then convert back to the local time zone for publishing results. This recommendation is illustrated in Listing 7-59. This example illustrates the problem of finding a duration between two datetimes when the duration includes the transition date from Daylight Savings to Standard Time.

We start by calling the localize() method to create the tm_end datetime object and subtract a Timedelta of one (1) week to create the tm_start_est datetime object. The datetime manipulation logic is straightforward and yet, notice, however, the tzinfo attribute for the tm_start_est object returns Eastern Standard Time, instead of correctly returning Eastern Daylight Time. This is displayed calling the first print() function in the program.
>>> nl           = ' '
>>> 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
>>>
... 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
Listing 7-59

Convert to UTC for Datetime Arithmetic

The preferred approach to solving this problem is to convert the tm_end datetime object to the UTC time zone, subtract a Timedelta of one (1) week, and then convert the new datetime object back to the US Eastern time zone. These steps are accomplished with the syntax
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)

Now the correct start datetime is returned from the tm_start_edt object as shown by calling the second print() function in the program.

SAS Time zone

As we have seen, Python handles time zone for datetime objects by assigning values to the tzinfo attribute , which is a component of the datetime object itself. The SAS implementation for time zone is handled by setting options for the execution environment using the SAS System option TIMEZONE. Unless otherwise set, the default value is blank (ASCII 32) 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 US Eastern.

Listing 7-60 illustrates calling PROC OPTIONS to return the current value for the TIMEZONE options . 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 this example, it returns a blank indicating the SAS TIMEZONE option is not set. Some SAS environment may have this option set as a restricted option and cannot be overridden.
4   proc options option=timezone;
5   run;
    SAS (r) Proprietary Software Release 9.4  TS1M5
 TIMEZONE=         Specifies a time zone.
6
7  data _null_;
8     local_dt = datetime();
9     tz_name  = tzonename();
10     put 'Default Local Datetime: ' local_dt e8601LX. /
11         'Default Timezone:       ' tz_name;
12  run;
OUTPUT:
Default Local Datetime:  2019-02-04T10:54:52-05:00
Default Timezone:
Listing 7-60

SAS Time Zone Option

Despite the fact an explicit SAS TIMEZONE option is not in effect, calling the DATETIME function returns a datetime representing the local time for the Eastern US. This is indicated by using the SAS-supplied, ISO 8601 e8601LX. datetime format which writes the datetime and appends the time zone difference between the local time and UTC. ISO 8601 is an international standard for representing dates, time, and interval values. SAS supports basic and extended ISO 8601 date, time, datetime, and interval values.

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:
  • B8601DX w .

  • E8601DX w .

  • B8601LX w .

  • E8601LX w .

  • B8601TX w .

  • E8601TX w .

  • NLDATMZ w .

  • NLDATMTZ w .

  • NLDATMWZ w .

We need to understand the behavior of the TIMEZONE option and its interactions with the aforementioned functions and formats since datetime values can be altered in unforeseen ways. Let’s begin by considering Listing 7-61 covering the TIMEZONE-related functions.7 The TZONEID, TZONENAME, TZONEDSTNAME, and TZONESTTNAME functions return strings representing the time zone ID, Name, time zone Daylight Savings Name, and time zone Standard Time name. The TZONEOFF , TZONESTTOFF , and TZONEDSTOFF function s return numerics representing the local time zone offset from UTC, the local UTC offset when standard time is in effect, and the UTC offset when Daylight Savings is in effect. In cases where Daylight Savings is not observed, for example, China, the TZONEDTNAME function returns a blank and the TZONEDSTOFF returns missing (.).
4  options tz='America/New_York';
5
6  data _null_;
7     tz_ny_id       = tzoneid();
8     tz_ny_name     = tzonename();
9     tz_ny_dst_name = tzonedstname();
10    tz_ny_st_name  = tzonesttname();
11    tz_ny_off      = tzoneoff();
12    tz_ny_off_dst  = tzonesttoff();
13    tz_ny_off_st   = tzonedstoff();
14
15 put 'TZ ID:                 ' tz_ny_id       /
16     'TZ Name:               ' tz_ny_name     /
17     'Daylight Savings Name: ' tz_ny_dst_name /
18     'Standard Time Name:    ' tz_ny_st_name   //
19     'TZ Offset from UTC:    ' tz_ny_off      /
20     'TZ DST Offset from UTC ' tz_ny_off_st   /
21     'TZ STD Offset from UTC ' tz_ny_off_dst  /;
22 run;
OUTPUT:
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
Listing 7-61

SAS Time zone Functions

We can use the TZONEOFF function to find the time difference between time zone, which is illustrated in Listing 7-62. Since time zone offsets west of the prime meridian (the location for UTC) are negative and those east are positive, we take the difference between the UTC offsets and take the absolute value.
4 options tz=“;
5
6 data _null_;
7
8   local_utc_offset = tzoneoff();
9   dn_frm_utc       = tzoneoff('America/Denver');
10  mo_frm_utc       = tzoneoff('Africa/Mogadishu');
11
12  diff_tz_hr       = abs((dn_frm_utc) - (mo_frm_utc)) / 3600;
13
14  put 'Denver UTC Offset:  ' dn_frm_utc    /
15      'Mogadishu UTC Offset: ' mo_frm_utc    /
16      'Timezone Difference:  ' diff_tz_hr ' Hours' //
17      'Local UTC Offset:     ' local_utc_offset;
18  run;
OUTPUT:
Denver UTC Offset:  -25200
Mogadishu UTC Offset: 10800
Timezone Difference:  10  Hours
Local UTC Offset:     -18000
Listing 7-62

SAS Time Zone Differences

And regardless of whether an instance of SAS has the TIMEZONE option explicitly set, or is implied, the TZONEOFF function always returns the number of seconds between the local time zone (which is obtained from the OS when not explicitly set) and UTC. In this case, the local UTC offset is for the Eastern US. As we shall see subsequently, the TZONEOFF function automatically takes into account transitions from Daylight Savings and Standard Time.

Consider Listing 7-63 to understand how datetimes are written and subsequently read with different TIMEZONE options in effect. This example creates a SAS datetime with the TIMEZONE option set for the US Eastern time zone and subsequently changed to create another datetime in the Shanghai, Asia time zone. The SAS TIMEZONE option, alias TZ, is set by supplying either a time zone ID or time zone name. This program executes three Data Steps. The first Data Step does the following:
  • Sets the TIMEZONE option to 'America/New_York'

  • Creates the NY dataset with two variables:
    • ny_dt initialized by calling the DATETIME function returning the local time for this time zone

    • ny_tz created by calling the TZONENAME function returning the time zone id in effect, in this example, EST, for Eastern Standard Time

  • Uses the PUT statement to write these values to the log.

The second Data Step performs similar logic:
  • Sets the TIMEZONE option set to 'Asia/Shanghai'

  • Creates the sha dataset with two variables:
    • sha_dt initialized by calling the DATETIME function returning the local time for this time zone

    • sha_tz created by calling the TZONENAME function returning the time zone id in effect, in this example, CST, for China Standard Time

  • Uses the PUT statement to write these values to the log.

The third Data Step sets the TIMEZONE option to 'America/New_York', merges the ny and sha datasets, calculates the time difference between the time zone, and uses the PUT statement to write these datetime values to the SAS log.
4 %let dt_fmt = dateampm.;
5
6 options tz='America/New_York';
7    data ny;
8    ny_dt = datetime();
9    ny_tz = tzonename();
10
11 put 'Time Zone in Effect: ' ny_tz /
12     'Local Date Time:     ' ny_dt &dt_fmt;
13 run;
OUTPUT:
Time Zone in Effect: EST
Local Date Time:     05FEB19:03:24:08 PM
NOTE: The dataset WORK.NY has 1 observations and 2 variables.
14
15 options tz='Asia/Shanghai';
16 data sha;
17    sha_dt = datetime();
18    sha_tz = tzonename();
19
20 put 'Time Zone in Effect: ' sha_tz /
21      'Local Date Time:    ' sha_dt &dt_fmt;
22 run;
OUTPUT:
Time Zone in Effect: CST
Local Date Time:     06FEB19:04:24:08 AM
NOTE: The dataset WORK.SHA has 1 observations and 2 variables.
23
24 options tz='America/New_York';
25 data both;
26    merge ny
27          sha;
diff_tz = abs(tzoneoff('America/New_york', ny_dt) -
28                 tzoneoff('Asia/Shanghai', sha_dt)) /3600;
29
30 put 'New York Datetime Read:     ' ny_dt   &dt_fmt  /
31     'Shanghai Datetime Read:     ' sha_dt  &dt_fmt / /
32 'Time Difference NY and SHANGHAI: ' diff_tz ' Hours';
33 run;
OUTPUT:
New York Datetime Read:      05FEB19:03:24:08 PM
Shanghai Datetime Read:      06FEB19:04:24:08 AM
Time Difference NY and SHANGHAI: 12  Hours
Listing 7-63

Setting SAS Time zone Option

The first thing to notice is how values returned by the DATETIME function are a function of the TIMEZONE option in effect. Secondly, writing datetime values created with one TIMEZONE option in effect and subsequently reading them using a different TIMEZONE option does not alter the values.

Which then raises the question of converting SAS datetimes written with a TIMEZONE option in effect to datetimes for another time zone? One approach is to utilize the TZONEOFF function together with the INTNX function illustrated in Listing 7-64. The purpose of this example is to illustrate datetime conversions using the INTCK function to shift datetimes based on time zone offsets.

Listing 7-64 executes two Data Steps. The first Data Step does the following:
  • Sets the TIMEZONE option to 'Asia/Shanghai'

  • Creates the sha dataset with one variable:
    • sha_dt_loc initialized with a DO/END block creating two datetime values of midnight November 3 and 4 local time. The BY value 86400 is the number of seconds in a day, returning two datetimes.

The second Data Step
  • Sets the TIMEZONE option set to 'Asia/Shanghai'

  • Creates these variables:
    • ny_utc calling the TZONEOFF function to find the datetime’s Eastern time zone offset from UTC in seconds. Dividing this value by 3600 converts to hours. This variable is for illustration and not used in a calculation.

    • sha_utc calling the TZONEOFF function to find the datetime’s Shanghai time zone offset from UTC in seconds. Dividing this value by 3600 converts to hours. This variable is for illustration and not used in a calculation.

    • diff_tz calling the TZONEOFF function twice, in order to find the difference of the Eastern time zone offset and the Shanghai offset. Since returned offsets can be negative, we take the absolute value.

    • sha_2_ny_tm calling the INTNX function to convert the datetime from the Shanghai time zone to the US Eastern time zone where with these four parameters:
      1. 1.

        'seconds' indicates the interval used to shift the datetime value.

         
      2. 2.

        sha_dt_loc is the datetime variable to be shifted. Its value was created with the TIMEZONE option set for Shanghai.

         
      3. 3.

        diff_tz variable contains the number of seconds the sha_dt_loc variable is to be shifted.

         
      4. 4.

        'same' is the optional alignment parameter, in this case, aligned for midnight for each datetime value.

         
    • diff_tz_hr to convert is the diff_tz variable from seconds to hours. This variable is for illustration and not used in a calculation.

4 %let dt_fmt = dateampm.;
5
6 options tz='Asia/Shanghai';
7 data sha;
8    do sha_dt_loc = '03Nov2019:00:00'dt to
9                    '04Nov2019:00:00'dt by 86400;
10        output;
11        put 'Shanghai Datetime Local:  ' sha_dt_loc &dt_fmt;
12    end;
13 run;
OUTPUT:
Shanghai Datetime Local:  03NOV19:12:00:00 AM
Shanghai Datetime Local:  04NOV19:12:00:00 AM
NOTE: The dataset WORK.SHA has 2 observations and 1 variables.
14  options tz='America/New_York';
15  data ny;
16      set sha;
17
18  ny_utc      = tzoneoff('America/New_york',
    sha_dt_loc)/3600;
19  sha_utc     = tzoneoff('Asia/Shanghai', sha_dt_loc)/3600;
20
21 diff_tz    = abs(tzoneoff('America/New_york', sha_dt_loc) -
22              tzoneoff('Asia/Shanghai', sha_dt_loc)) ;
23
24 sha_2_ny_tm = intnx('seconds', sha_dt_loc, diff_tz, 'same');
25
26 diff_tz_hr  = diff_tz / 3600;
27
28 put 'Sha Local DT Read:    ' sha_dt_loc   &dt_fmt /
29     'Sha DT to NY DT:      ' sha_2_ny_tm  &dt_fmt /
30     'Time zone Difference: ' diff_tz_hr /
31     'New York UTC Offset:  ' ny_utc /
32     'Shanghai UTC Offset:  ' sha_utc //;
33
34 run;
OUTPUT:
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
Listing 7-64

SAS Time zone Conversions

The dates chosen for this example illustrate the behavior of the TZONEOFF function when dealing with datetime values that include transition dates from Daylight Savings to Standard Time in the Eastern time zone.

Summary

As more data comes from a highly instrumented world, analysis tasks will need to effectively deal with datetime data. In this chapter we discussed dates, times, and datetimes relevant to many data analysis tasks. Understanding how the Python Standard Library handles datetimes provides a foundation for understanding how the pandas Library processes dates. We also detailed how to effectively deal with dates and datetimes across time zone.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.128.33