Accurately calculating business hours between two time stamps in Web Intelligence - sap

I am trying to calculate the number of working / business hours between two timestamps at the report level in Web Intelligence.
So far, I have been able to calculate the total number of hours between the two dates, the total # of business days excluding weekends.
Method for business days:
Business Days variable = [Days Between Assignment + Completion]-(2 * [# of weekends])
Days between variable := TimeBetween([Assignment Date / Time];[Completion Date / Time];DayPeriod)
Calculate weekends := DaysBetween(LastDayOfWeek(Min([Assignment Date / Time]));LastDayOfWeek([Completion Date / Time])) / 7
Variables created:
BusinessStartTime = 8:00 AM
BusinessEndTime = 5:00 PM
BusinessDaysBetweenStartandEnd
TimeBetweenStartandEnd
The difficulty is getting the calculations to only count between the business hours of 8:00 AM and 5:00 PM.
Example timestamps of both objects:
Start time: Sep 27, 2019 7:53:27 AM
Completion time: Oct 9, 2019 2:29:24 PM
Using SAP BusinessObjects BI Platform 4.2, and the report I am creating is being done through Web Intelligence.

My approach makes a number of assumptions…
You have a method to count the number of business days with in a date
range. At my company we have a calendar table that designates a each
day as a business day or not. Your company likely observes different
holidays than mine anyway.
The first and last days of the date range
are actually business days.
So here is the basic approach…
Determine how long a business day is in minutes.
Find the number of full business days in the specified date range.
Find the number of business minutes on the first day.
Find the number of business minutes on the last day.
Add up all the minutes.
Convert to hours and minutes.
You could probably do this in one variable, but that makes it difficult to understand what is going on. I am going to use a lot of variables to show the process of how this calculation is done.
First, create two variables to define the beginning and end of the business day in 24-hour time. You could do 12-hour time, but that would be more complex.
Beginning of Day="8:00"
End of Day="17:00"
Now create a series of variables related to the business day. The Beginning of Day Minutes and End of Day Minutes variables are the number of minutes since midnight, respectively.
Beginning of Day Today=ToDate(FormatDate(CurrentDate(); "yyyy-MM-dd") + " " + [Beginning of Day]; "yyyy-MM-dd H:mm")
End of Day Today=ToDate(FormatDate(CurrentDate(); "yyyy-MM-dd") + " " + [End of Day]; "yyyy-MM-dd H:mm")
Beginning of Day Minutes=(ToNumber(FormatDate([Beginning of Day Today]; "H"); "#") * 60) +ToNumber(FormatDate([Beginning of Day Today]; "mm"); "#")
End of Day Minutes=(ToNumber(FormatDate([End of Day Today]; "H"); "#") * 60) +ToNumber(FormatDate([End of Day Today]; "mm"); "#")
Business Day Minutes=[End of Day Minutes] - [Beginning of Day Minutes]
We are ready to start dealing with the particular datetimes between which we are trying to find the business hours. Create two variables to hold the start and completion values.
Start Time=ToDate("2019-09-27 7:53:27 AM"; "yyyy-MM-dd h:mm:ss a")
Completion Time=ToDate("2019-10-09 2:29:24 PM"; "yyyy-MM-dd h:mm:ss a")
Next, I want to create variables to find the number of minutes since midnight for each of my date range values.
Start Time Minutes=(ToNumber(FormatDate([Variables].[Start Time]; "H"); "#") * 60) + ToNumber(FormatDate([Variables].[Start Time]; "mm"); "#")
Completion Time Minutes=(ToNumber(FormatDate([Variables].[Completion Time]; "H"); "#") * 60) + ToNumber(FormatDate([Variables].[Completion Time]; "mm"); "#")
We have laid the foundation. Now we are ready to really dive in. We need to find the number of full business days. For simplicity, I am using the DaysBetween() function. Your calculation is going to be specific to your organization.
Full Business Days=DaysBetween([Variables].[Start Time];[Variables].[Completion Time]) - 1
Let’s figure out how many business minutes are in the range on the first and last days. If the start time is before the beginning of the business day, we will consider it to be a full day only. Likewise, if the completion time is after the end of the business day, we will consider it to be a full day only.
Minutes on Start Day=If([Start Time Minutes] < [Beginning of Day Minutes]; [Business Day Minutes]; [End of Day Minutes] - [Start Time Minutes])
Minutes on Completion Day=If([Completion Time Minutes] > [End of Day Minutes]; [Business Day Minutes]; [Completion Time Minutes] - [Beginning of Day Minutes])
Now we can calculate the total minutes and then convert it to hours and minutes.
Business Minutes=([Full Business Days] * [Business Day Minutes]) + [Minutes on Start Day] + [Minutes on Completion Day]
Business Hours and Minutes=FormatNumber(Floor([Business Minutes] / 60) ;"#") + ":" + FormatNumber(Mod([Business Minutes]; 60); "0#")
I created input controls tied to the Beginning of Day and End of Day values to easily adjust them.
To show how this would work with the results from a query I created a free-hand SQL query with the following SQL. This is for SQL Server. I am not sure how this might need to change for other database platforms.
SELECT '2019-09-27 7:53:27 AM' AS [Start Time]
, '2019-10-09 2:29:24 PM' AS [Completion Time]
UNION
SELECT '2019-12-15 1:19:35 PM' AS [Start Time]
, '2019-12-16 4:31:11 PM' AS [Completion Time]
UNION
SELECT '2019-12-28 8:02:473 AM' AS [Start Time]
, '2019-12-28 4:17:34 PM' AS [Completion Time]
UNION
SELECT '2019-12-30 9:02:13 AM' AS [Start Time]
, '2020-01-03 11:53:58 PM' AS [Completion Time];
All I need to do is change the two variables with my datetime range values to be based on the query rather than the hard-coded values.
Start Time=[Query 1].[Start Time]
Completion Time=[Query 1].[Completion Time]
Hopefully, you can adapt this approach to your situation.
Noel

Related

GROUP BY different dates for max and min numbers

I am trying to query this data set of hourly price date. The dataset defined daily prices at 12am - 12am UTC time, I am trying to define the days at 4pm - 4pm UTC time. Therefore I need to get the high and the low prices for each day between ex: '2021-12-15 16:00:00' and '2021-12-16 15:00:00' as that would be the open and close of the trading day.
I have this right now:
SELECT convert(date,dateadd(S, TimeStamp/1000, '1970-01-01')) as 'date'
,symbol
,Max([high]) as 'Max'
,Min([low]) as 'Min'
FROM [Crypto].[tblMessariPriceHistory]
WHERE symbol = 'DOGE'
and dateadd(S, TimeStamp/1000, '1970-01-01') between '2021-12-15 16:00:00' and '2021-12-16 15:00:00'
Group By convert(date,dateadd(S, TimeStamp/1000, '1970-01-01')),symbol
But it results like this:
date
symbol
Max
Min
2021-12-15
DOGE
0.175059052503167
0.170510833636204
2021-12-16
DOGE
0.180266282681554
0.177596458601872
I could just group by Symbol but I want to be able to do this over multiple days, and that wouldn't work.
Any ideas on how to define a select date range as a group or table over multiple days?
If you think about it, subtracting 16 h off every time would slide the time back to some time within the "starting day"
Monday 16:00 becomes midnight Monday
Monday 23:59 becomes 7:59 Monday
Tuesday 00:00 becomes 8:00 Monday
Tuesday 15:59 becomes 23:59 Monday
Tuesday 16:00 becomes midnight Tuesday
Anyway, once you've slid your time backwards 16h, you can just chop the time part off by dividing the unix time stamp by the number of milliseconds in a day and all the trades between Monday 16:00 and Tuesday 15:59:59.999 will go down as "Monday". If it were a DateTime we could cast it to a Date to achieve the same thing. It's handy to find ways of treating datetimes as decimal numbers where the integral is the date and the fractional is the time because chopping it to an int discards the time and allows daily aggregating. If you wanted hourly aggregating, adjusting the number so it represents the number of hours and fractions of an hour (divide by 3600000, the number of milliseconds in an hour) helps to the same end
--take off 16h then truncate to number of days since epoch
SELECT
(timestamp-57600000)/86400000 as timestamp,
symbol,
min(low) as minlow,
max(high) as maxhigh
FROM trades
GROUP BY (timestamp-57600000)/86400000 as timestamp, symbol

Working days between two dates in Snowflake

Is there any ways to calculate working days between two dates in snowflake without creating calendar table, only using "datediff" function
After doing research work on snowflake datediff function, I have found the following conclusions.
DATEDIFF(DAY/WEEK, START_DATE, END_DATE) will calculate difference, but the last date will be considered as END_DATE -1.
DATEDIFF(WEEK, START_DATE, END_DATE) will count number of Sundays between two dates.
By summarizing these two points, I have implemented the logic below.
SELECT
( DATEDIFF(DAY, START_DATE, DATEADD(DAY, 1, END_DATE))
- DATEDIFF(WEEK, START_DATE, DATEADD(DAY, 1, END_DATE))*2
- (CASE WHEN DAYNAME(START_DATE) != 'Sun' THEN 1 ELSE 0 END)
+ (CASE WHEN DAYNAME(END_DATE) != 'Sat' THEN 1 ELSE 0 END)
) AS WORKING_DAYS
Here's an article with a calendar table solution that also includes a UDF to solve this in Snowflake (the business days are hard-coded, so that does require some maintenance, but you don't have to maintain a calendar table at least):
https://medium.com/dandy-engineering-blog/how-to-calculate-the-number-of-working-hours-between-two-timestamps-in-sql-b5696de66e51
The best way to count the number of Sundays between two dates is possibly as follows:
CREATE OR REPLACE FUNCTION SUNDAYS_BETWEEN(a DATE,b DATE)
RETURNS INTEGER
AS $$
FLOOR( (DAYOFWEEKISO(a) + DATEDIFF('days',a,b)) / 7 ,0)
$$
The above is better than using DATEDIFF(WEEK because the output of that function changes if the WEEK_START session parameter is altered away from the legacy default of 0
I have a way to calculate the number of business hours that elapse between a start time and end time but it only works if you make the following assumptions.
Asssume only 1 time zone for all timestamps
Any start or end times that occur outside of business hours should be rounded to nearest business hour time. (I.e. Assuming a schedule of 10:00am - 6:00 pm, timestamps occurring from midnight to 9:59am should be rounded to 10am, times after 6:00pm should be set to the next day at 10:00am)
Timestamps that occur on the weekends should be set to the opening time of the next business day. (In this case Monday at 10:00am)
My model does not account for any holidays.
If these 4 conditions are met then the following code should be enough for a rough estimate of business hours elapsed.
(DATEDIFF(seconds, start_time, end_time) --accounts for the pure number of seconds in between the two dates
- (DATEDIFF(DAY, start_time,end_time) * 16 * 60*60) --For every day between the two dates, we need to subtract out X number of hours. Where X is the number of hours not worked in a day. (i.e. for a standard 8 hour work day, set X =16. For a 10 hour day, set X = 14, etc.) We multiple by (60*60*16) to convert days into seconds.
- (DATEDIFF(WEEK, businness_hours_wait_time_start_at_est, businness_hours_first_touch_at_est)*(8*2*60*60)) --This accounts for the fact that weekends are not work days. Which is why we need to subtract an additional 8 hours for Saturday and Sunday.
)/(60*60*8) --We then divide by 60*60*8 to convert the business seconds into business days. We use 8 hours here instead of 24 hours since our "business day" is only 8 hours long.

Time elapsed between two dates (In a specific time range) ORACLE

I am creating a query that shows me the time elapsed between two dates, only taking into account only the one that is Monday through Friday from 08:00 to 17:00, for example:
For example, if a petition opens on day 1 at 6:30 p.m. and closes on day 2 at 8:45 p.m., the TMO is 45 minutes.
If it closes on day 3 at 8:45, the TMO is 9 hours and 45 minutes.
Example 2:
If a petition opens on Friday at 16:45 and closes on Tuesday at 8:30, the MTO would be: 15 minutes on Friday, nine hours on Monday and 30 minutes on Tuesday for an MTO = 9 hours 45 minutes
The query is performed on a single column of type date as I show below
I currently use a LAG function to make the query, but I can not create something functional, not even optimal to incorporate, I would greatly appreciate your help.
In the solution below I will ignore the "lag" part of your problem, which you said you know how to use. I am only showing how to count "working hours" between any two date_times (they may be during or before or after work hours, and/or they can be on weekend days; the computation is the same in all cases).
Explaining the answer in words: For two given date-times, "start" and "end", calculate how many "work" hours elapsed from the beginning of the week (from Monday 00:00:00) till each of them. This is in fact a calculation for ONE date, not for TWO dates. Then: given "start" and "end", calculate this number of hours for each of them; subtract the "end" number of hours from the "start" number of hours. To the result, add x times 5 times 9, where x is the difference in weeks between Monday 00:00:00 of the two dates. (If they are in the same week, the difference will be 0.)
To truncate a date to the beginning of the day, we use TRUNC(dt). To truncate to the beginning of Monday, TRUNC(dt, 'iw').
To compute how many "work" hours are from the beginning of the date dt until the actual time-of-day we can use the calculation
greatest(0, least(17/24, dt - trunc(dt)) - 8/24)
(the results will be in days; we calculate everything in days and then we can convert to hours). However, in the final formula we must check to see if the date is a Saturday or Sunday, in which case this should just be zero. Or, better, we can adjust the calculation a bit later, when we count from the beginning of Monday (we can use least( 5*9/24, ...)).
Putting everything together:
with
inputs ( dt1, dt2 ) as (
select to_date('2017-09-25 11:30:00', 'yyyy-mm-dd hh24:mi:ss'),
to_date('2017-10-01 22:45:00', 'yyyy-mm-dd hh24:mi:ss')
from dual
)
-- End of SIMULATED input dates (for testing only).
select 24 *
( least(5 * (17 - 8) / 24, greatest(0, least(17/24, dt2 - trunc(dt2)) - 8/24)
+ (17 - 8) / 24 * (trunc(dt2) - trunc(dt2, 'iw')))
-
least(5 * (17 - 8) / 24, greatest(0, least(17/24, dt1 - trunc(dt1)) - 8/24)
+ (17 - 8) / 24 * (trunc(dt1) - trunc(dt1, 'iw')))
+ 5 * (17 - 8) / 24 * (trunc(dt2, 'iw') - trunc(dt1, 'iw')) / 7
)
as duration_in_hours
from inputs
;
DURATION_IN_HOURS
-----------------
41.500

SQL Server Date Logic - Finding the next reminder date

Given the following database table:
StartDate DATETIME -- day that the reminder period starts
LastReminderDate DATETIME -- the last time the reminder triggered
DayOfMonth INT -- the day of the month to remind the user
Interval INT -- how often in months to remind the user
How can I figure out the next reminder date based on these values? For example:
StartDate = '6/1/2011'
LastReminderDate = '6/5/2011'
DayOfMonth = 5 -- remind on the 5th of the month
Interval = 2 -- remind every other month
For this particular example, the next reminder date should be 8/5/2011 because it reminds on the 5th of the month every two months. How would I write a function to figure this out?
If LastReminderDate is NULL, then LastReminderDate should be equal to StartDate
UPDATE:
StartDate = '6/1/2011'
LastReminderDate = NULL
DayOfMonth = 5
Interval = 2
In this case, there was no last reminder date. The first time the reminder would occur would be 6/5/2011. The solutions below seem to be returning 8/5 in this case.
Here are some specific rules:
The Reminder should always occur on whatever DayOfMonth is. If DayOfMonth would be illegal for the given month then it should be the last day of that month. For example....if DayOfMonth is 31 and the next reminder date would fall on June 31, then it should be June 30th instead.
The next reminder date should always be based off of the Last Reminder Date plus the Interval. If Last Reminder Date does not match the Day of Month, then it could potentially be more than what the interval was. For example...if Last Reminder was 6/1/2011 and the interval is 2 months, but the reminder is for the 20th of the month, then the next reminder will be 8/20/2011.
If there is no last reminder date, then use the Start Date instead of last reminder date...but this will use the earliest date in the future. If start date was 6/1/2011 and day of month is 5, then this will be 7/5/2011 since today is 6/22/2011. If Day of Month was 25 then it would be 6/25/2011
DECLARE
#StartDate AS datetime, -- day that the reminder period starts
#LastReminderDate AS datetime, -- the last time the reminder triggered
#DayOfMonth AS integer, -- the day of the month to remind the user
#Interval AS integer -- how often in months to remind the user
SET #StartDate = '6/1/2011'
SET #LastReminderDate = '6/5/2011'
SET #DayOfMonth = 5 -- remind on the 5th of the month
SET #Interval = 2 -- remind every other month
SELECT
CASE
WHEN #LastReminderDate IS NULL
THEN
CASE WHEN Day(#StartDate) <= #DayOfMonth
THEN DateAdd( month, ((Year( #StartDate ) - 1900) * 12) + Month( #StartDate ) - 1, #DayOfMonth - 1 )
ELSE DateAdd( month, ((Year( #StartDate ) - 1900) * 12) + Month( #StartDate ) - 0, #DayOfMonth - 1 )
END
ELSE DateAdd( month, #Interval, #LastReminderDate )
END
The meat of this is the last four lines, the SELECT CASE ... END statement. I provided a whole script that lets you plug in different values, and see how the SELECT CASE ... END behaves for those test values.
But to just use this on your table, use only the last four lines (and remove the # from the front of the names so they match the table's column names).
You could also generalize this so that Interval doesn't have to be months. If your table had an IntervalType column, you could supply that as the first argument to DateAdd(). See the docs but some common intervals are days, months, years, and so on.
EDIT2: Respect DayOfMonth.
Since you want to use the StartDate if there is no LastReminderDate, then you'll want to use COALESCE for that bit of logic: COALESCE(LastReminderDate, StartDate)
Now, get to the last of the previous month: DATEADD(DAY, -DAY(COALESCE(LastReminderDate, StartDate)), COALESCE(LastReminderDate, StartDate))
Finally, add the months and then get to the date that we need:
DATEADD(MONTH, Interval, DATEADD(DAY, -DAY(COALESCE(LastReminderDate, StartDate)) + DayOfMonth, COALESCE(LastReminderDate, StartDate)))
This will potentially go forward less than two months if the date of the last reminder was on a day of the month after the "DayOfMonth" that's configured for the reminder. You should be able to tweak that depending on what your business logic is in that situation.

How to Calculate Working Hours including Minutes?

I have a query that fetches employee's name, date, working hours, wage and calculate salary based on working hours * wage.
The minutes in working hours that is being calculated are discarded. I only get the value in full hour. Snapshot example:
My main concern is on workingHours and wageAmount
workingHours is displayed as 5. Here is the time sheet snapshot:
As you see shift starts at 10:30 AM and ends at 4:00 PM (excluding lunch time), so the actual total hours is 4 and a half not 5.
Here is the query snippet that calculates the hours:
DATEDIFF(HOUR, shiftStart.timeEntered, shiftEnd.timeEntered) - DATEDIFF(HOUR, lunchStart.timeEntered, lunchEnd.timeEntered) AS workingHours,
MY QUESTION IS:
1. How can i calculate working hours accurately with minutes included (discard seconds)?
Since i will be counting the minutes, i will have to calculate the wage per minute so i will have to put the following code that calculates wage per minute:
ROUND((((DATEDIFF(HOUR, shiftStart.timeEntered, shiftEnd.timeEntered) - DATEDIFF(HOUR, lunchStart.timeEntered, lunchEnd.timeEntered)) * 60) * (cast(((w.wageAmount/60)-((w.wageAmount/60)%.001)) as decimal (18,3)))), 0) AS "SalaryPerMin",
Use DATEDIFF(minute, shiftStart.timeEntered, shiftEnd.timeEntered) * 1.0 / 60 instead, to give you the hour as a decimal.
Time card stuff is very business rule intensive. I would consider your architecture before putting all the rules in sp's.
This will return the Hours & Minutes:
CAST(DATEDIFF(MINUTE, StartTime, EndTime) AS DECIMAL)/60