質問

I have a database which contains a table of time clock setup entries. This serves as the "buckets" of time an employee's day could fall into. This table can contain any number of buckets as long as they don't overlap eachother.

For example, this is our table of buckets:

ID  Start           End         timeType      
-------------------------------------------------   
1   08:00:00.000    12:00:00.000    REGULAR 
1   12:00:00.000    12:30:00.000    BREAK
1   12:30:00.000    16:00:00.000    REGULAR
1   16:00:00.000    00:00:00.000    OVERTIME

I have a punch in time of say, 07:55 and a punch out time of 17:00. I need to figure out how much of my day falls into each bucket, in hours, minutes and seconds. The data output has to look like this and I can not add columns to either table:

ID  Start           End         timeType     hrs
-----------------------------------------------------   
1   07:55:00.000    12:00:00.000    REGULAR      4.08
1   12:00:00.000    12:30:00.000    BREAK        0.50
1   12:30:00.000    16:00:00.000    REGULAR      3.50
1   16:00:00.000    00:00:00.000    OVERTIME     1.00

I'm thinking a SQL inline table valued function that will be run for one day at a time, but I am having trouble getting to the hours calculation piece. So far, I think I have the logic for all scenarios, I just need help with calculating the hours as a decimal(5,2) for each scenario. I'm putting this out there for SQL suggestions but also...am I over complicating this?

Here's my stab at the logic for each scenario:

Select Case When CONVERT(time, @PunchInDate) <= CONVERT(time, EndDate) 
And CONVERT(time, @PunchInDate) >= CONVERT(time, StartDate) 
AND CONVERT(time, @PunchOutDate) <= CONVERT(time, EndDate) 
AND CONVERT(time, @PunchOutDate) >= CONVERT(time, StartDate)Then
    'Starts and ends in this range.'
Else
    ''
End as ScenarioA
, Case  
When CONVERT(time, @PunchInDate) <= CONVERT(time, StartDate) 
AND CONVERT(time, @PunchInDate) <= CONVERT(time, EndDate) 
AND  CONVERT(time, @PunchOutDate) >= CONVERT(time, StartDate) 
AND  CONVERT(time, @PunchOutDate) <= CONVERT(time, EndDate) 
Then
    'Starts before this range and ends in this range'
Else
    ''
End as ScenarioB
, Case  
When CONVERT(time, @PunchInDate) >= CONVERT(time, StartDate) 
And CONVERT(time, @PunchInDate) <= CONVERT(time, EndDate)
And CONVERT(time, @PunchOutDate) >= CONVERT(time, StartDate) 
And CONVERT(time, @PunchOutDate) >= CONVERT(time, EndDate)Then
'Starts in this range and ends after the range'
Else ''
END as ScenarioC
, Case
When CONVERT(time, @PunchInDate) <= CONVERT(time, StartDate) 
And CONVERT(time, @PunchInDate) >= CONVERT(time, EndDate)
And CONVERT(time, @PunchOutDate) >= CONVERT(time, StartDate) 
And CONVERT(time, @PunchOutDate) >= CONVERT(time, EndDate)Then
'Starts before this range and ends after the range'
Else ''
END as ScenarioD
From MyTable
Where  EmpID = @EmpID
役に立ちましたか?

解決

If you don't already have one, create a number table. This is just a table with a single int column that contains all the integers between 0 and some large number (my table goes to 9999).

create table #numbers(num int)
insert #numbers
SELECT TOP 10000 row_number() over(order by t1.number) -1 as N
FROM master..spt_values t1 
CROSS JOIN master..spt_values t2

create table #bucket(Id int, StartTime time, EndTime time, TimeType varchar(10))
insert #bucket
select 0, '00:00:00', '08:00:00', 'OTHER' union
select 1, '08:00:00', '12:00:00', 'REGULAR' union
select 2, '12:00:00', '12:30:00', 'BREAK' union
select 3, '12:30:00', '16:00:00', 'REGULAR' union
select 4, '16:00:00', '00:00:00', 'OVERTIME' 

declare @punchInDate datetime
declare @punchOutDate datetime

set @punchInDate = '5/15/2014 7:55'
set @punchOutDate = '5/15/2014 17:00'

--Using the number table, break the punchIn and punchOut times into individual rows for each minute and store them in a temp table.    
select convert(time, dateadd(mi, n.num, @punchInDate)) TimeMinute
into #temp
from #numbers n
where n.num <= datediff(mi, @punchInDate, @punchOutDate)
order by 1

--Now you can just join your temp rows with your bucket table, grouping and getting the count of the number of minutes in each bucket.
select b.Id, b.StartTime, b.EndTime, b.TimeType, convert(decimal, COUNT(t.TimeMinute))/60
from #bucket b
join #temp t on t.TimeMinute>= b.StartTime and t.TimeMinute <= dateadd(mi, -1, b.EndTime)
group by b.Id, b.StartTime, b.EndTime, b.TimeType

他のヒント

Converting Datetime or Time Bucket Values to Numbers

You should probably store the bucket Start and End values as int values, either instead of the times or in addition-to.

To convert datetime values or expressions into something you can use for matching to the buckets, you can use something like in this code:

DECLARE @when datetime = GETDATE();

SELECT DATEDIFF(minute, DATEADD( day, DATEDIFF(day, 0, @when), 0 ), @when);

--MinutesSinceStartOfDate
-------------------------
--858

--(1 row(s) affected)

If you need seconds, just change the above to something like this:

DECLARE @when datetime = GETDATE();

SELECT DATEDIFF(second, DATEADD( day, DATEDIFF(day, 0, @when), 0 ), @when);

--SecondsSinceStartOfDate
-------------------------
--52466

--(1 row(s) affected)

Matching a Range (Start and End Values) to Buckets (Partitions)

Here's some code that you should be able to adapt to match 'buckets':

CREATE TABLE #buckets (
    Id      int,
    Start   int,
    Finish  int
);
GO

INSERT #buckets ( Id, Start, Finish )
VALUES  ( 1,  8, 12 ),
        ( 2, 12, 13 ),
        ( 3, 13, 16 ),
        ( 4, 16, 24 );

DECLARE @beginning  int = 9,
        @ending     int = 17;

SELECT  x.*,
        EffectiveInterval = x.EffectiveFinish - x.EffectiveStart
FROM (  SELECT  *,
                EffectiveStart  = CASE WHEN Start < @beginning  THEN @beginning ELSE Start END,
                EffectiveFinish = CASE WHEN Finish > @ending    THEN @ending    ELSE Finish END
        FROM #buckets
        WHERE   Finish >= @beginning
                AND Start <= @ending
    ) x;


DROP TABLE #buckets;

--(4 row(s) affected)
--Id          Start       Finish      EffectiveStart EffectiveFinish EffectiveInterval
------------- ----------- ----------- -------------- --------------- -----------------
--1           8           12          9              12              3
--2           12          13          12             13              1
--3           13          16          13             16              3
--4           16          24          16             17              1

--(4 row(s) affected)

Comparison of your Code and Mine

CREATE TABLE #buckets (
    Id      int,
    Start   time,
    Finish  time
);


INSERT #buckets ( Id, Start, Finish )
VALUES  ( 1, '08:00', '12:00' ),
        ( 2, '12:00', '12:30' ),
        ( 3, '12:30', '16:00' ),
        ( 4, '16:00', '11:59:59.999' );



DECLARE @PunchInDate datetime = '2014-05-15 07:55',
        @PunchOutDate datetime = '2014-05-15 17:00';



Select Case When CONVERT(time, @PunchInDate) <= CONVERT(time, Finish) 
And CONVERT(time, @PunchInDate) >= CONVERT(time, Start) 
AND CONVERT(time, @PunchOutDate) <= CONVERT(time, Finish) 
AND CONVERT(time, @PunchOutDate) >= CONVERT(time, Start)Then
    'Starts and ends in this range.'
Else
    ''
End as ScenarioA
, Case  
When CONVERT(time, @PunchInDate) <= CONVERT(time, Start) 
AND CONVERT(time, @PunchInDate) <= CONVERT(time, Finish) 
AND  CONVERT(time, @PunchOutDate) >= CONVERT(time, Start) 
AND  CONVERT(time, @PunchOutDate) <= CONVERT(time, Finish) 
Then
    'Starts before this range and ends in this range'
Else
    ''
End as ScenarioB
, Case  
When CONVERT(time, @PunchInDate) >= CONVERT(time, Start) 
And CONVERT(time, @PunchInDate) <= CONVERT(time, Finish)
And CONVERT(time, @PunchOutDate) >= CONVERT(time, Start) 
And CONVERT(time, @PunchOutDate) >= CONVERT(time, Finish)Then
'Starts in this range and ends after the range'
Else ''
END as ScenarioC
, Case
When CONVERT(time, @PunchInDate) <= CONVERT(time, Start) 
And CONVERT(time, @PunchInDate) >= CONVERT(time, Finish)
And CONVERT(time, @PunchOutDate) >= CONVERT(time, Start) 
And CONVERT(time, @PunchOutDate) >= CONVERT(time, Finish)Then
'Starts before this range and ends after the range'
Else ''
END as ScenarioD
FROM #buckets;



DECLARE @PunchInTime time,
        @PunchOutTime time;

SELECT  @PunchInTime    = CONVERT(time, @PunchInDate),
        @PunchOutTime   = CONVERT(time, @PunchOutDate);


SELECT  x.*,
        EffectiveInterval = DATEDIFF(second, x.EffectiveStart, x.EffectiveFinish),
        EffectiveIntervalDecimalHourse = DATEDIFF(second, x.EffectiveStart, x.EffectiveFinish) / 3600.
FROM (  SELECT  *,
                EffectiveStart  = CASE WHEN Start < @PunchInTime    THEN @PunchInTime ELSE Start END,
                EffectiveFinish = CASE WHEN Finish > @PunchOutTime  THEN @PunchOutTime  ELSE Finish END
        FROM #buckets
        WHERE   Finish >= @PunchInTime
                AND Start <= @PunchOutTime
    ) x;


DROP TABLE #buckets;



--(4 row(s) affected)
--ScenarioA                      ScenarioB                                       ScenarioC                                     ScenarioD
-------------------------------- ----------------------------------------------- --------------------------------------------- -------------------------------------------------





--(4 row(s) affected)

--Id          Start            Finish           EffectiveStart   EffectiveFinish  EffectiveInterval EffectiveIntervalDecimalHourse
------------- ---------------- ---------------- ---------------- ---------------- ----------------- ---------------------------------------
--1           08:00:00.0000000 12:00:00.0000000 08:00:00.0000000 12:00:00.0000000 14400             4.000000
--2           12:00:00.0000000 12:30:00.0000000 12:00:00.0000000 12:30:00.0000000 1800              0.500000
--3           12:30:00.0000000 16:00:00.0000000 12:30:00.0000000 16:00:00.0000000 12600             3.500000
--4           16:00:00.0000000 11:59:59.9990000 16:00:00.0000000 11:59:59.9990000 -14401            -4.000277

--(4 row(s) affected)

If you're using SQLServer 2012 you can get the expected result in a single select.

declare @punchInDate datetime = '5/15/2014 7:55'
declare @punchOutDate datetime = '5/15/2014 17:00';

WITH punchTime AS (
  SELECT punchIn = cast(@punchInDate AS Time)
       , punchOut = cast(@punchOutDate AS Time)
), T AS (
SELECT b.ID
     , b.StartTime, b.EndTime
     , pt.punchIn, pt.punchOut
     , sIn = SUM(CASE WHEN pt.punchIn < b.StartTime
                      THEN 1
                      ELSE 0
                 END) OVER (ORDER BY ID)
     , sOut = SUM(CASE WHEN pt.punchOut > b.StartTime
                       THEN 1
                       ELSE 0
                  END) OVER (ORDER BY ID DESC)
FROM   bucket b
       CROSS JOIN punchTime pt
), C AS (
SELECT ID
     , StartTime = CASE sIn WHEN 1 THEN punchIn ELSE StartTime END
     , EndTime = CASE sOut WHEN 1 THEN punchOut ELSE EndTime END
FROM   T
)
SELECT ID
     , StartTime
     , EndTime
     , hrs = Cast(DateDiff(mi, StartTime, EndTime) / 60.0 AS Decimal(4, 2))
FROM   C
ORDER BY ID 

SQLFiddle demo (in the demo the punch times are in a table)

Using SUM OVER(ORDER BY) we get a rolling sum.
sIn will be 1 in the first row where StartTime is after punchIn.
sOut will be 1 in the last row where StartTime is before punchOut.
With those pointers is easy to substitute the punch time to the standard bucket time and get the worked hours.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top