-
Notifications
You must be signed in to change notification settings - Fork 41
FEAT/Calculate business days elapsed based on two inputs #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 6 commits
249d93a
dc5d2f3
ec66603
a732a55
73cba53
b1157ee
3253a67
0fa6a07
c6b0b33
50c2d5a
563dcda
e2b8b24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| {% macro create_udfs() %} | ||
|
|
||
| create schema if not exists {{target.schema}}; | ||
|
|
||
| {{ create_udf_business_days() }}; | ||
| {{ create_udf_business_hours() }}; | ||
|
|
||
| {% endmacro %} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| {% macro create_udf_business_days() %} | ||
|
|
||
| create or replace function {{target.schema}}.udf_business_days(date1 TIMESTAMP_NTZ, date2 TIMESTAMP_NTZ, country STRING) | ||
| returns int | ||
| language python | ||
| runtime_version = 3.8 | ||
| packages = ('numpy','holidays') | ||
| handler = 'business_days_py' | ||
| as | ||
|
|
||
| $$ | ||
| import numpy as np | ||
| import holidays as holidays | ||
|
|
||
| def business_days_py(start_date, end_date, country='US'): | ||
|
|
||
| # calculate business days between two dates | ||
| start_date = start_date.date() | ||
| end_date = end_date.date() | ||
| years = [*range(1990,2030)] | ||
|
|
||
| holiday_list = list(holidays.country_holidays(country, years=years)) | ||
|
|
||
| return np.busday_count(start_date, end_date, holidays=holiday_list) | ||
|
|
||
| $$ | ||
|
|
||
| {% endmacro %} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| version: 2 | ||
|
|
||
| macros: | ||
| - name: create_udf_business_days | ||
| description: > | ||
| This macro creates a UDF within the target schema that can be used to | ||
| calculate business days (excluding holidays) between two date fields | ||
| in a Snowflake table. The macro mearly creates the UDF within Snowflake, | ||
| and is called by the create_udfs macro. You must ensure that create_udfs | ||
| is called on-run-start in dbt_project.yml. | ||
|
|
||
| To utilize the UDF within your dbt project, proper syntax is: | ||
|
|
||
| select | ||
| *, | ||
| udf_business_days(start_date, end_date, country) | ||
| as business_days_elapsed | ||
| from sample_table | ||
|
|
||
| Notes: | ||
| - The date fields passed MUST be type == date. | ||
| - The country field is optional as it is defaulted to 'US'. If looking to | ||
| use a different country, must use the ISO 3166-1 alpha-2 country code | ||
| as defined here: | ||
| https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes | ||
| - This UDF only considers holidays between Jan 1 1990 and Dec 31 2030, as | ||
| is currently hardcoded. This can be changed in the macro source code. | ||
| - If increased granularity is desired for the region, you must modify the | ||
| macro to apply regional (state or province) holidays as described as | ||
| defined here: | ||
| holiday_list = holidays.country_holidays(country, subdiv='PR') | ||
| - The holidays python package does not include the end date in its | ||
| calculations of the duration. To account for this, you can add a day to | ||
| your end date to make the calculation inclusive. | ||
|
|
||
| docs: | ||
| show: false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| {% macro create_udf_business_hours() %} | ||
|
|
||
| create or replace function {{target.schema}}.udf_business_hours(start_datetime TIMESTAMP_NTZ, end_datetime TIMESTAMP_NTZ, country STRING) | ||
| returns number | ||
| language python | ||
| runtime_version = 3.8 | ||
| packages = ('numpy','holidays') | ||
| handler = 'business_hours_py' | ||
| as | ||
|
|
||
| $$ | ||
| from datetime import datetime | ||
| from datetime import timedelta | ||
| import holidays | ||
| import numpy as np | ||
|
|
||
| def business_hours_py(start_datetime, end_datetime, country='US'): | ||
|
|
||
| # Year range for holidays spanning the years below | ||
| years = [*range(1990,2030)] | ||
|
|
||
| # Hard coded business hours, opening and closing times | ||
| workhours_per_day = 8 | ||
| opening_hour = 9 | ||
| closing_hour = 17 | ||
|
|
||
| # Create open and closing datetimes to establish day 0 and day n durations | ||
| first_day_closed = start_datetime.replace(hour=closing_hour, minute=0, second=0) | ||
| last_day_opened = end_datetime.replace(hour=opening_hour, minute=0, second=0) | ||
|
|
||
| holiday_list = list(holidays.country_holidays(country, years=years)) | ||
| days = np.busday_count( | ||
| start_datetime.date(), | ||
| end_datetime.date(), | ||
| holidays = holiday_list) | ||
|
|
||
| # Calculate hours for full business days (excluding first day and last day) | ||
| complete_days_hours = (days - 2) * workhours_per_day | ||
|
|
||
| # Jump forward to the next non-holiday weekday | ||
| while start_datetime.date() in holiday_list or start_datetime.weekday() in [5,6]: | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After your review, going to talk to Peter about practicality on omitting weekdays. we could make it a boolean tag in the call, (i.e. udf_business_hours(start_datetime, end_datetime, country, omit_weekends[Yes|No]) but it will complicate the code with more conditional if statements for the while loop L48 and the if statement L57 |
||
| start_datetime = start_datetime.replace( | ||
| hour = opening_hour, minute = 0, second = 0 | ||
| ) + timedelta(days = 1) | ||
|
cris-seaton marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Jump back to the last non-holiday weekday | ||
| while end_datetime.date() in holiday_list or end_datetime.weekday() in [5,6]: | ||
| end_datetime = end_datetime.replace( | ||
| hour = closing_hour, minute = 0, second = 0 | ||
| ) - timedelta(days = 1) | ||
|
|
||
| if start_datetime.date() == end_datetime.date(): | ||
| return round((end_datetime - start_datetime).seconds/60/60,2) | ||
| else: | ||
| duration_day_zero = round((first_day_closed - start_datetime ).seconds/60/60, 2) | ||
| duration_day_n = round((end_datetime - last_day_opened).seconds/60/60, 2) | ||
| return round(duration_day_zero + duration_day_n + complete_days_hours, 2) | ||
|
|
||
| $$ | ||
|
|
||
| {% endmacro %} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| version: 2 | ||
|
|
||
| macros: | ||
| - name: create_udf_business_hours | ||
| description: > | ||
| This macro creates a UDF within the target schema that can be used to | ||
| calculate business hours (excluding holidays) between two date fields | ||
| in a Snowflake table. The macro mearly creates the UDF within Snowflake, | ||
| and is called by the create_udfs macro. You must ensure that create_udfs | ||
| is called on-run-start in dbt_project.yml. | ||
|
|
||
| To utilize the UDF within your dbt project, proper syntax is: | ||
|
|
||
| select | ||
| *, | ||
| udf_business_days(start_datetime, end_datetime, country) | ||
| as business_hours_elapsed | ||
| from sample_table | ||
|
|
||
| Notes: | ||
| - The date fields passed MUST be type == datetime (i.e. timestamp_ntz). | ||
| - The country field is optional as it is defaulted to 'US'. If looking to | ||
| use a different country, must use the ISO 3166-1 alpha-2 country code | ||
| as defined here: | ||
| https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes | ||
| - This UDF only considers holidays between Jan 1 1990 and Dec 31 2030, as | ||
| is currently hardcoded. This can be changed in the macro source code. | ||
| - If increased granularity is desired for the region, you must modify the | ||
| macro to apply regional (state or province) holidays as described as | ||
| defined here: | ||
| holiday_list = holidays.country_holidays(country, subdiv='PR') | ||
| - The holidays python package does not include the end date in its | ||
| calculations of the duration. To account for this, you can add a day to | ||
| your end date to make the calculation inclusive. | ||
|
|
||
| docs: | ||
| show: false |
Uh oh!
There was an error while loading. Please reload this page.