Query to calculate partial labor hours - sql

I'm not sure if it's possible to do exactly what I'm wanting, but hopefuly someone here can give me some direction. I'm trying to do some labor reports and a few of them require me to make comparisons to labor by hour.
Starting as simple as possible, I would like to know how I could query over employee In and Out times for a given hour and find out how many total hours have been worked.
For example, if I have:
id In Out
-- ------- ------
1 12:00pm 8:30pm
2 12:00pm 8:15pm
3 8:15pm 11:00pm
and I wanted to query for how many hours of labor there were in the 8 o'clock hour then I would expect to see a result of 2 hours (or 120 minutes... 30m + 15m + 45m).
Then moving on from there I'd like to write a query that will show this information for the whole day, but group on the hour showing this same information for each hour. I realize there are other concerns (i.e. I would also need to group on the date, etc.) but if I can figure this out then the rest will be a breeze.
For more context, you can see a similar question I have here: Query for labor cost by hour
(The difference between this question and the other one, if it's not clear, is that I am asking about a specific approach here. If you think you have a better approach to this problem, then please add it to the other question.)

Try this to get your started, need to tweak it a bit, but should give you what you want on your sample data...
select id,
minutes - case when inMinutes < 0 then 0 else inminutes end as TotalMins
from
(
select id,
case when datediff(mi,'8:00pm',OutTime) >60 then 60 else datediff(mi,'8:00pm',OutTime) end as Minutes,
case when datediff(mi,'8:00pm',InTime) >60 then 60 else datediff(mi,'8:00pm',InTime) end as InMinutes
from testhours
) xx

If that is a Microsoft system then datetime values can be converted to double precision floating point numbers where the decimal point divides the date and time like this:
integer part: number of days since 1900-01-01
decimal part: time in the day (so that 0.5 = 12:00:00)
So you could use something like this:
-- sample datetime values
declare #in datetime, #out datetime
set #in = '2012-01-18 12:00:00'
set #out = '2012-01-18 20:30:00'
-- calculation
declare #r_in real, #r_out real
set #r_in = cast(#in as real)
set #r_out = cast(#out as real)
select (24 * (#r_out - #r_in)) as [hours]
BUT it is not that precise, so I would recommend this for calculation:
select cast(datediff(second, #in, #out) as real)/3600 as [hours]

OK, I would also suggest to use functions because that's maybe a lot slower on large tables but also easier and more simple to code.
But here's another solution without functions:
-- the day you're interested in:
declare #day datetime
set #day = '2012-01-20'
-- sample data
declare #moves table (id int, tin datetime, tout datetime)
insert into #moves values
(1, '2012-01-20 06:30:00', '2012-01-20 15:45:00'),
(2, '2012-01-20 13:05:00', '2012-01-20 19:45:00'),
(3, '2012-01-20 10:10:00', '2012-01-20 10:50:00'),
(4, '2012-01-20 19:35:00', '2012-01-20 21:00:00')
-- a helper table with hours
declare #i int
declare #hours table (h int, f datetime, t datetime)
set #i = 0
while #i < 24
begin
insert into #hours values(#i, dateadd(hour, #i, #day), dateadd(hour, #i + 1, #day))
set #i = #i + 1
end
-- here's the code
select h.h,
sum(case sign(datediff(second, h.f, m.tin))
when 1 then
case sign(datediff(second, m.tout, h.t))
when 1 then datediff(minute, m.tin , m.tout)
else datediff(minute, m.tin , h.t)
end
when null then null
else
case sign(datediff(second, m.tout, h.t))
when 1 then datediff(minute, h.f, m.tout)
else datediff(minute, h.f, h.t)
end
end) as minutesWorked,
count(distinct m.id) as peopleWorking
from #hours h inner join #moves m
-- on h.f >= m.tin and h.t <= m.tout
on h.f <= m.tout and h.t >= m.tin
group by h.h
order by h.h
This will give you the following results:
h minutesWorked peopleWorking
----------- ------------- -------------
6 30 1
7 60 1
8 60 1
9 60 1
10 100 2
11 60 1
12 60 1
13 115 2
14 120 2
15 105 2
16 60 1
17 60 1
18 60 1
19 70 2
20 60 1
21 0 1

Related

Round time to exact hour SQL Server

I have this SQL query:
select
e.Cedula,
concat (e.Nombre, ' ', e.Apellido) nombre,
C.Descripcion cargo,
max(case when mo.Sentido = 'Entrada' then cast(mo.FechaHora as time)
end) as Entrada,
max(case when mo.Sentido = 'Salida' then cast(mo.FechaHora as time)
end) as Salida,
e.Direccion observaciones
from
mambo.dbo.EMPLEADO e
left join
mambo.dbo.CARGO c on e.IdCargo = c.IdCargo
left join
mambo.dbo.MARCACIONES_PARA_LIQUIDACION mo on e.IdEmpleado = mo.IdEmpleado
and CAST(mo.FechaHora AS DATE) = '2018-04-25'
where
e.IdCentroCosto = 14
and e.estado = '1'
group by
e.IdEmpleado, e.Cedula, e.Nombre, e.Apellido, c.Descripcion, e.Direccion
Which returns this result:
Result of Select
Is there a way I could round the time to the nearest exact hour in a range of 15 minutes?
Example:
if it is 6:45 I want the result to show 7:00
if it is 6:58 I want the result to show 7:00
if it is 17:15 I want the result to show 17:00
if it is 17:01 I want the result to show 17:00
Any help would be very appreciated
Hmmm . . . how about just constructing the value yourself?
datename(hour, dateadd(minute, 30, max(case when mo.Sentido = 'Entrada' then mo.FechaHora end))) + ':00' as Entrada,
The addition of 30 minutes is to facilitate rounding.
You can turn this into a function, some of it can be simplified/removed it is in there to show steps/clarity.
DECLARE #TimeCheck AS VARCHAR(50)
SET #TimeCheck = '17:30'
DECLARE #HourOfTime AS INT
DECLARE #MinuteOfTime AS INT
SET #MinuteOfTime = RIGHT(#TimeCheck, 2)
SET #HourOfTime = LEFT(#TimeCheck, 2)
SELECT #HourOfTime, #MinuteOfTime
-- here if the minutes between 0 and 20 then dont add anything, if it is 30 and 59 add 1 to the hours.
-- if you need more options or need to change the logic you can add more WHEN statements or change the numbers for the between
SET #HourOfTime += CASE
WHEN #MinuteOfTime BETWEEN 0 AND 29 THEN 0
WHEN #MinuteOfTime BETWEEN 30 AND 59 THEN 1
END
SELECT CAST(#HourOfTime AS VARCHAR(30)) + ':00' -- since rounding this will just be hard coded to 00

if the date difference between two dates is X do this if is Y do something else

My start date is 2009-04-01 (4th of april 2009), end date is getdate().
I need to calculate for every date from the start date until today some things.
I also need to break it into two cases:
case 1: if it's a friday i need to go back 347 days , example
select datediff(dw, '2010-11-22','2011-11-04')
for every friday, then perform some selects.
case 2: if it's from monday to thursday then go back 349 days, example
select datediff(dw, '2010-11-19','2011-11-03')
How can I write this, what I need to perform is not relevant just put it as:
declare #startDate date
declare #endDate date
declare #dateHolder date
declare #tsID int
set #startDate = '2009-04-01'
set #endDate = getdate()
set datefirst 1;
while (select count (tsID) from #tempI)>0
begin
select top 1 #tsID = tsID from #tempI
while (select count (rateDate) from #tempI)>0
begin
select top 1 #dateHolder = rateDate from #tempI
case (select datename (dw, #dateHolder) = '5' then someColumn = #dateHolder - 347 as dateIneedToUseForMyFormula
case (select datename (dw, #dateHolder) = '1' or '2' or '3' or '4' then someColumn = #dateHolder - 349 as dateIneedToUseForMyFormula
-- here i don't know how to write the code, so i'll write pseudo
-- i have tsID rateDate and rate
-- i need to put in a new column (the value obtained from taking the value
-- from the column rate corresponding to the #dateHolder - the value from
-- the column rate corresponding to the #dateHolder - 347 or 349
-- depending on the case) * 100
end
delete from #tempI where #dateHolder = rateDate
end
delete from #tempI where #tsID = tsID
end
EDIT
I was asked in the comments what to do, and given the downvotes, I don't think i made myself clear. I will copy paste what i wrote in the comment:
"
first to iterate through all the tsID in the list, after doing that to iterate through all the dates, get a date subtract 349 or 347 days from it, then check put in a new column the result of : the value of the 'rate' column which corresponds to my date - the value of the 'rate' column which corresponds to the date from 349 or 347 days ago. This performed for all the dates for every id "
edit 2: Expected output
tsID rateDate rate calculated
1 2009-04-01 0.12 null
1 2009-04-02 0.14 null
1 2009-04-03 0.11 null
2 2009-04-01 0.01 null
2 2009-04-02 0.012 null
2 2009-04-03 0.43 null
. . . 347 days later or 349 depending
on what 2009-04-01 was
1 2010-03-16 0.454 (0.454 - 0.12)*100
1 2010-03-17 0.34 (0.34 - 0.14)*100
1 2010-03-18 0.9 (0.9 - 0.11)*100
then same for id 2.3...4...
Try This
Declare #start Date='2009-09-01', #end Date=getdate();
;With NumberSequence( Number ) as
(
Select #start as Number
union all
Select DATEADD(d,1,Number)
from NumberSequence
where Number < #end
)
Select
(CASE WHEN (datepart(dw,Number) =6) THEN DATEADD(d,-347,Number)
ELSE DATEADD(d,-349,Number)
END ) AS Date
From NumberSequence Option (MaxRecursion 10000)
Use a case statement with weekday:
Select Case
When WeekDAy(yourdate, 3) < 4 then datediff(dw, '2010-11-19','2011-11-03')
when WeekDAy(yourdate, 3) = 4 then datediff(dw, '2010-11-22','2011-11-04')
Else ...?... End

Sql Server 2008 Calculate nights between two dates but remove 31st night

I have a booking table with the following information:
BookingID,(unique, not null)
StartDate, (not null)
EndDate (not null)
I need to calculate the number of nights someone remained in residence which I can do with a DATEDIFF between EndDate and StartDate. However, if someone is in residence for the entire month during a 31 day month we only charge them 30.
I'm not sure how to do this in SQL. I was thinking I would have to create a variable, calculate on a monthly basis and add to the variable, but that seems like it would be messy and take a very long time, especially towards the end of year. This needs to be done for about 5,000 records on a daily basis.
So:
If someone starts on 7/25/14 and ends 9/2/14, the nights is 38 not 39.
If someone starts on 10/2/14 and ends on 11/1/14, the nights is 30.
If someone starts on 10/2/14 and ends on 10/31/14, the nights is 29.
We will be calculating into the future so it doesn't matter if the end date is greater than the day the report is being ran.
Does anyone have any ideas how to accomplish this in the best way?
I would first to create a lookup table with all the month with 31 days
Such as
DECLARE #month TABLE (start_date date,end_date date)
INSERT INTO #month VALUES ('2014-07-01','2014-07-31'),('2014-08-01','2014-08-31'),('2014-10-01','2014-10-31'),('2014-12-01','2014-12-31')
//populate all months in your calculate range
Then you can calculate the value with
DECLARE #start DATE = '2014-07-25', #end DATE = '2014-09-02'
SELECT DATEDIFF(day,#start,#end) -
(SELECT COUNT(*) FROM #month WHERE start_date >= #start AND end_date <= #end)
Retrieve the integer part of the datediff divided by 31 :
SELECT DATEDIFF(day,'2014-07-25', '2014-09-02') - DATEDIFF(day,'2014-07-25', '2014-09-02') / 31
The SQLish solution is to create a calendar table that holds all the dates you will ever care about and any business meaning that those dates may have, such as "Is this a holiday?", "Is this off-season?", or "do we charge for this day?"
This may sound insane to someone accustomed to other programming languages, but it is perfectly sensible in the database world. Business rules and business logic get stored as data, not as code.
Make this table and populate it:
CREATE TABLE Calendar (
[date] date
,[is_charged] bit
)
and then you can write code that is nearly plain English:
SELECT
[BookingID]
,COUNT([date])
FROM BookingTable
INNER JOIN Calendar
ON [date] >= [StartDate]
AND [date] < [EndDate]
WHERE [is_charged] = 1
GROUP BY [BookingId]
When your business rules change, you just update the calendar instead of rewriting the code.
If I've read your question correctly then you can't actually use those solutions above which consist of a table of billable and non billable days because the 31st is billable unless the whole month was booked.
I reckon this is probably a job for a user defined function. Which runs up a total starting with the month that the start date is in and finishing with the month that the end date is in.
CREATE FUNCTION dbo.FN_BillableDays (#StartDate date, #EndDate date)
returns int
AS
BEGIN
IF #StartDate > #EndDate
BEGIN
return null --Error
END
DECLARE #Next date
DECLARE #MonthStart date
DECLARE #MonthEnd date
DECLARE #NextMonthStart date
DECLARE #n int =0
SET #Next = #StartDate
SET #MonthStart = DATEADD(day,1-DAY(#Next),#Next)
SET #NextMonthStart = DATEADD(month,1,#MonthStart )
SET #MonthEnd = DATEADD(day,-1,#NextMonthStart)
WHILE DATEDIFF(month,#Next,#EndDate) >0
BEGIN
SET #n = #n +
CASE
WHEN DAY(#next) = 1 AND DAY(#MonthEnd) = 31 THEN 30
WHEN DAY(#next) = 1 THEN DAY(#MonthEnd)
ELSE 1+DAY(#MonthEnd) -DAY(#next) END
SET #MonthStart = #NextMonthStart
SET #NextMonthStart = DATEADD(month,1,#MonthStart )
SET #MonthEnd = DATEADD(day,-1,#NextMonthStart)
SET #Next = #NextMonthStart
END
--Month of the EndDate
SET #n = #n +
CASE
WHEN DAY(#next) = 1 AND DAY(#EndDate) = 31 THEN 29
WHEN DAY(#next) = 1 THEN DAY(#EndDate)-1
ELSE DAY(#MonthEnd) -DAY(#EndDate) END
return #n
END
I tried it with some test dates
SELECT
b.BookingID,
b.StartDate,
b.EndDate,
dbo.FN_BillableDays (b.StartDate,b.EndDate) AS BillableDays
FROM dbo.Booking b
And got the following
BookingID StartDate EndDate BillableDays
----------- ---------- ---------- ------------
1 2013-12-31 2014-01-02 2
2 2013-12-31 2014-01-30 30
3 2014-01-01 2014-01-30 29
4 2014-01-01 2014-01-31 29
5 2014-01-01 2014-02-01 30
6 2014-01-01 2014-02-02 31
7 2014-02-02 2014-02-01 NULL
(7 row(s) affected)
Which matches my understanding of the logic you want to implement but you may want to tweak the last bit which adds on the days for the final month. If they leave on the 31st do you want to give them their last night (30th to 31st) for free.
If you don't then delete the line
WHEN DAY(#next) = 1 AND DAY(#EndDate) = 31 THEN 29

Random Scheduling

I have the following table in my database:
tbl1
PK
ClientID
ScheduleDay
Time1Start
Time1Stop
Time2Start
Time2Stop
Time3Start
Time3Stop
Status
Here is some sample data
ID ClientID ScheduleDay Time1Start Time1Stop Time2Start Time2Stop Time3Start Time3Stop
-- -------- ----------- ---------- --------- ---------- --------- ---------- ---------
1 3 Sunday 0000 0800 1000 1300 NULL NULL
2 3 Monday 0000 2359 NULL NULL NULL NULL
3 3 Tuesday 1000 1200 1330 1700 1900 2200
4 3 Wednesday 0000 0800 NULL NULL NULL NULL
5 3 Thursday 0800 1200 NULL NULL NULL NULL
6 3 Friday 0400 0800 0900 1600 NULL NULL
The Time fields are CHAR(4) since I am storing the time in a military format.
What I need to accomplish is this; for any given ClientID, insert one or more records into a schedule table with the time value of the record being within the time frames in tbl1. For example, scheduling ClientID 3 on Tuesday, the time scheduled could be 1120.
In the event that multiple records need to be inserted, the times scheduled should not be any closer than one hour.
Any and all help is appreciated!
Here's my best guess as to what you're trying to do. The first two parts of the CTE are really just to get things into a form similar to what FlyingStreudel suggests. Ideally, you should change the database to match that format instead of doing this through CTEs. That will make this significantly simpler and is better for data integrity as well.
Next, I just get the distinct start times in hour increments. You could do that by joining to a Numbers table as well if you can't use CTEs (you didn't mention the version of SQL Server that you're using).
Finally, I grab one of those start times at random, using the RAND function and ROW_NUMBER. You'll want to set a good seed value for RAND().
;WITH TimesAsTimes AS
(
SELECT
ScheduleDay,
CAST(SUBSTRING(T1.Time1Start, 1, 2) + ':' + SUBSTRING(T1.Time1Start, 3, 2) AS TIME) AS time_start,
CAST(SUBSTRING(T1.Time1Stop, 1, 2) + ':' + SUBSTRING(T1.Time1Stop, 3, 2) AS TIME) AS time_stop
FROM
tbl1 T1
WHERE
T1.Time1Start IS NOT NULL
UNION ALL
SELECT
ScheduleDay,
CAST(SUBSTRING(T2.Time2Start, 1, 2) + ':' + SUBSTRING(T2.Time2Start, 3, 2) AS TIME) AS time_start,
CAST(SUBSTRING(T2.Time2Stop, 1, 2) + ':' + SUBSTRING(T2.Time2Stop, 3, 2) AS TIME) AS time_stop
FROM
tbl1 T2
WHERE
T2.Time2Start IS NOT NULL
UNION ALL
SELECT
ScheduleDay,
CAST(SUBSTRING(T3.Time3Start, 1, 2) + ':' + SUBSTRING(T3.Time3Start, 3, 2) AS TIME) AS time_start,
CAST(SUBSTRING(T3.Time3Stop, 1, 2) + ':' + SUBSTRING(T3.Time3Stop, 3, 2) AS TIME) AS time_stop
FROM
tbl1 T3
WHERE
T3.Time3Start IS NOT NULL
),
PossibleTimeStarts AS
(
SELECT
ScheduleDay,
time_start,
time_stop
FROM
TimesAsTimes TAT
UNION ALL
SELECT
ScheduleDay,
DATEADD(hh, 1, time_start) AS time_start,
time_stop
FROM
PossibleTimeStarts PTS
WHERE
DATEADD(hh, 1, time_start) <= DATEADD(hh, -1, PTS.time_stop)
),
PossibleTimesWithRowNums AS
(
SELECT
ScheduleDay,
time_start,
ROW_NUMBER() OVER(PARTITION BY ScheduleDay ORDER BY ScheduleDay, time_start) AS row_num,
COUNT(*) OVER(PARTITION BY ScheduleDay) AS num_rows
FROM
PossibleTimeStarts
)
SELECT
*
FROM
PossibleTimesWithRowNums
WHERE
row_num = FLOOR(RAND() * num_rows) + 1
First of all you may want to try a schema like
tbl_sched_avail
PK id INT
FK client_id INT
day INT (1-7)
avail_start varchar(4)
avail_end varchar(4)
This way you are not limited to a finite number of time fences.
As far as checking the schedules availability -
CREATE PROCEDURE sp_ins_sched
#start_time varchar(4),
#end_time varchar(4),
#client_id INT,
#day INT
AS
BEGIN
DECLARE #can_create BIT
SET #can_create = 0
DECLARE #fence_start INT
DECLARE #fence_end INT
--IS DESIRED TIME WITHIN FENCE FOR CLIENT
DECLARE c CURSOR FOR
SELECT avail_start, avail_end FROM tbl_sched_avail
WHERE client_id = #client_id
AND day = #day
OPEN c
FETCH NEXT FROM c
INTO #fence_start, #fence_end
WHILE ##FETCH_STATUS = 0 AND #can_create = 0
BEGIN
IF #start_time >= #fence_start AND #start_time < #fence_end
AND #end_time > #fence_start AND <= #fence_end
SET #can_create = 1
FETCH NEXT FROM c
INTO #fence_start, #fence_end
END
CLOSE c
DEALLOCATE c
IF #can_create = 1
BEGIN
--insert your schedule here
END
END
As far as the code for actually inserting the record I would need to know more about the tables in the database.

TSQL Selecting 2 & 3 week intervals

I am attempting to populate a table based on 2 and 3 week intervals for a semi-monthly pay period in TSQL. The table should populate,
2 week date
2 week date
3 week date
2 week date
2 week date
3 week date
..based on the first date I supply, subsequently adding 2 or 3 weeks to the last date supplied. I should be able to supply a start date and end date. It may be that it's just early in the morning, but can't think of an elegant way to accomplish this task. Any pointers?
Thanks!
George
WITH dates (d, n) AS
(
SELECT #mydate, 1
UNION ALL
SELECT DATEADD(week, CASE n % 3 WHEN 0 THEN 3 ELSE 2 END, d), n + 1
FROM dates
WHERE d < #enddate
)
INSERT
INTO mytable
SELECT d
FROM dates
OPTION (MAXRECURSION 0)
Horrid brute force approach - because the 2,2,3 is difficult to loop just adding it regardless into the temp table and then filtering at the end incase a couple extra entries go in - not the most efficient but if you are needing to just get a range one off then it works.
So the caveat here is: ok for one off, I wouldn't use in production :)
declare #start datetime
declare #end datetime
declare #calculated datetime
set #start = '20010101'
set #end = '20011231'
set #calculated = #start
Create Table #Dates (PayDate datetime)
while #calculated <= #end
begin
set #calculated = DateAdd(wk,2,#calculated)
insert into #Dates(paydate) values (#calculated)
set #calculated = DateAdd(wk,2,#calculated)
insert into #Dates(paydate) values (#calculated)
set #calculated = DateAdd(wk,3,#calculated)
insert into #Dates(paydate) values (#calculated)
end
select * from #Dates where paydate >= #start and paydate <= #end
drop table #dates
So you have a 7-week cycle -- figure out which 7-week period you're in from some known starting point and then which week of this group of 7 you are.