Home > Back-end >  Get specific series values in order
Get specific series values in order

Time:10-19

I have a table contains the info of the access time and movements for each employee.

Some employees have shift work hours between two days, I have column determine if this access time belongs to the previous or current day.

I need to collect the access time which belongs to one day, then get the difference between the current access time and the next one and determine the check status of the movement(first or last or during the day movement).

I get the mentioned result if the employee has shift work hours on the same day but I can't get the true result if he has work hours between two days.

Example:

Employee Number 1 has Shift method: 17:00 -> 01:00

Employee Number 2 has Shift method: 08:30 -> 16:30

Total Work Hours: 08:00

   CREATE TABLE My_Table (
  EMP_ID    NUMBER(4) ,
  Timeinout date ,
  flag number(1)
 
);

INSERT INTO My_Table VALUES (1,to_date('03-07-2018 16:39:44','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (1,to_date('04-07-2018 01:14:40','dd-mm-yyyy hh24:mi:ss'),1);
INSERT INTO My_Table VALUES (1,to_date('04-07-2018 01:14:44 ','dd-mm-yyyy hh24:mi:ss'),1);
INSERT INTO My_Table VALUES (1,to_date('04-07-2018 16:14:52','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (1,to_date('05-07-2018 01:07:40','dd-mm-yyyy hh24:mi:ss'),1);
INSERT INTO My_Table VALUES (1,to_date('05-07-2018 01:07:44 ','dd-mm-yyyy hh24:mi:ss'),1);
INSERT INTO My_Table VALUES (1,to_date('05-07-2018 16:31:08','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (1,to_date('06-07-2018 01:01:48 ','dd-mm-yyyy hh24:mi:ss'),1);
INSERT INTO My_Table VALUES (1,to_date('06-07-2018 01:01:52','dd-mm-yyyy hh24:mi:ss'),1);
INSERT INTO My_Table VALUES (2,to_date('03-07-2018 08:37:40','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (2,to_date('03-07-2018 16:27:36','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (2,to_date('04-07-2018 08:37:04 ','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (2,to_date('04-07-2018 12:58:36','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (2,to_date('04-07-2018 13:09:48 ','dd-mm-yyyy hh24:mi:ss'),0);
INSERT INTO My_Table VALUES (2,to_date('04-07-2018 17:15:32 ','dd-mm-yyyy hh24:mi:ss'),0);


COMMIT;

The query I used is for employee number2 return the true result:

   select X.*,
       CASE
         WHEN Previous_Movment IS NULL AND Next_Movment IS NOT NULL THEN
          'First Check'
         WHEN Previous_Movment IS NOT NULL AND Next_Movment IS NULL THEN
          'Last Check'
         when Previous_Movment IS NULL AND Next_Movment IS NULL THEN
          'Do Not have access in this day'
         ELSE
          'During the Day'
       end CHECK_sTATUS
  from (select trunc(a.timeinout) as date_,
               a.timeinout current_movment,
               LEAD(TIMEINOUT) OVER(partition by trunc(TIMEINOUT), Emp_ID ORDER BY TIMEINOUT, Emp_ID) Next_Movment,
               LAG(TIMEINOUT) OVER(partition by trunc(TIMEINOUT), Emp_ID ORDER BY TIMEINOUT, Emp_ID) Previous_Movment,
               trunc(24 *
                     mod(LEAD(TIMEINOUT) OVER(partition by trunc(TIMEINOUT),
                              Emp_ID ORDER BY TIMEINOUT,
                              Emp_ID) - TIMEINOUT,
                         1)) as diff_hours,
               trunc(mod(mod(LEAD(TIMEINOUT)
                             OVER(partition by trunc(TIMEINOUT),
                                  Emp_ID ORDER BY TIMEINOUT,
                                  Emp_ID) - TIMEINOUT,
                             1) * 24,
                         1) * 60) as diff_minus,
               trunc(mod(mod(mod(LEAD(TIMEINOUT)
                                 OVER(partition by trunc(TIMEINOUT),
                                      Emp_ID ORDER BY TIMEINOUT,
                                      Emp_ID) - TIMEINOUT,
                                 1) * 24,
                             1) * 60,
                         1) * 60) as diff_sec,
               Emp_ID,
               FLAG Return_Previous_day_or_not
          from My_table a
         WHERE trunc(a.timeinout) between
               to_Date('03-07-2018', 'dd-mm-yyyy') and
               to_Date('07-07-2018', 'dd-mm-yyyy')
          and a.Emp_ID = 2
        
        ) X

and the get the below result for employee number2:

enter image description here

But when I switch the query to employee number1 I get the wrong result because the movements between two days:

enter image description here Note:

I used Trunc(Timeinout) in the LEAD & lAG function for the employee who has work hours on the same day.

CodePudding user response:

From Oracle 12, you can use MATCH_RECOGNIZE for this.

Assuming that you want a shift to last for no more than 12 hours (yours appear to be 8 hours and people check in early and leave later) then:

SELECT emp_id,
       timeinout AS current_movement,
       flag,
       CASE
       WHEN cls IN ('FIRST_CHECK', 'DURING_DAY')
       THEN next_timeinout
       END AS next_movement,
       CASE
       WHEN cls IN ('DURING_DAY', 'LAST_CHECK')
       THEN prev_timeinout
       END AS previous_movement,
       CASE
       WHEN cls IN ('FIRST_CHECK', 'DURING_DAY')
       THEN (next_timeinout - timeinout) DAY TO SECOND
       END AS diff,
       cls AS check_status
FROM   my_table
MATCH_RECOGNIZE(
  PARTITION BY emp_id
  ORDER BY timeinout
  MEASURES
    PREV(timeinout) AS prev_timeinout,
    NEXT(timeinout) AS next_timeinout,
    LAST(last_check.timeinout) AS last_timeinout,
    CLASSIFIER() AS cls
  ALL ROWS PER MATCH
  PATTERN (first_check (during_day* last_check)?)
  DEFINE
    during_day AS timeinout < first_check.timeinout   INTERVAL '12' HOUR,
    last_check AS timeinout < first_check.timeinout   INTERVAL '12' HOUR
) m

Which, for your sample data, outputs:

EMP_ID CURRENT_MOVEMENT FLAG NEXT_MOVEMENT PREVIOUS_MOVEMENT DIFF CHECK_STATUS
1 2018-07-03T16:39:44 0 2018-07-04T01:14:40 00 08:34:56.000000 FIRST_CHECK
1 2018-07-04T01:14:40 1 2018-07-04T01:14:44 2018-07-03T16:39:44 00 00:00:04.000000 DURING_DAY
1 2018-07-04T01:14:44 1 2018-07-04T01:14:40 LAST_CHECK
1 2018-07-04T16:14:52 0 2018-07-05T01:07:40 00 08:52:48.000000 FIRST_CHECK
1 2018-07-05T01:07:40 1 2018-07-05T01:07:44 2018-07-04T16:14:52 00 00:00:04.000000 DURING_DAY
1 2018-07-05T01:07:44 1 2018-07-05T01:07:40 LAST_CHECK
1 2018-07-05T16:31:08 0 2018-07-06T01:01:48 00 08:30:40.000000 FIRST_CHECK
1 2018-07-06T01:01:48 1 2018-07-06T01:01:52 2018-07-05T16:31:08 00 00:00:04.000000 DURING_DAY
1 2018-07-06T01:01:52 1 2018-07-06T01:01:48 LAST_CHECK
2 2018-07-03T08:37:40 0 2018-07-03T16:27:36 00 07:49:56.000000 FIRST_CHECK
2 2018-07-03T16:27:36 0 2018-07-03T08:37:40 LAST_CHECK
2 2018-07-04T08:37:04 0 2018-07-04T12:58:36 00 04:21:32.000000 FIRST_CHECK
2 2018-07-04T12:58:36 0 2018-07-04T13:09:48 2018-07-04T08:37:04 00 00:11:12.000000 DURING_DAY
2 2018-07-04T13:09:48 0 2018-07-04T17:15:32 2018-07-04T12:58:36 00 04:05:44.000000 DURING_DAY
2 2018-07-04T17:15:32 0 2018-07-04T13:09:48 LAST_CHECK

db<>fiddle here

CodePudding user response:

From Oracle 11gR2, you can use a recursive sub-query:

WITH ordered_data AS (
  SELECT m.*,
         ROW_NUMBER() OVER (PARTITION BY emp_id ORDER BY timeinout) AS rn,
         LAG(timeinout) OVER (PARTITION BY emp_id ORDER BY timeinout)
           AS previous_movement,
         LEAD(timeinout) OVER (PARTITION BY emp_id ORDER BY timeinout)
           AS next_movement
  FROM   my_table m
),
grouped_data (
  emp_id,
  current_movement,
  flag,
  rn,
  previous_movement,
  next_movement,
  grp,
  check_status,
  first_movement
) AS (
  SELECT emp_id,
         timeinout,
         flag,
         rn,
         NULL,
         next_movement,
         1,
         'FIRST_CHECK',
         timeinout
  FROM   ordered_data
  WHERE  rn = 1
UNION ALL
  SELECT o.emp_id,
         o.timeinout AS current_movement,
         o.flag,
         o.rn,
         CASE
         WHEN o.timeinout < g.first_movement   INTERVAL '12' HOUR
         THEN o.previous_movement
         ELSE NULL
         END,
         CASE
         WHEN o.timeinout < g.first_movement   INTERVAL '12' HOUR
         AND  (  o.next_movement >= g.first_movement   INTERVAL '12' HOUR
              OR o.next_movement IS NULL)
         THEN NULL
         ELSE o.next_movement
         END,
         CASE
         WHEN o.timeinout < g.first_movement   INTERVAL '12' HOUR
         THEN g.grp
         ELSE g.grp   1
         END,
         CASE
         WHEN o.timeinout < g.first_movement   INTERVAL '12' HOUR
         AND  (  o.next_movement >= g.first_movement   INTERVAL '12' HOUR
              OR o.next_movement IS NULL)
         THEN 'LAST_CHECK'
         WHEN o.timeinout < g.first_movement   INTERVAL '12' HOUR
         THEN 'DURING_DAY'
         ELSE 'FIRST_CHECK'
         END,
         CASE
         WHEN o.timeinout < g.first_movement   INTERVAL '12' HOUR
         THEN g.first_movement
         ELSE o.timeinout
         END
  FROM   ordered_data o
         INNER JOIN grouped_data g
         ON (o.emp_id = g.emp_id AND o.rn = g.rn   1)
)
SELECT emp_id,
       current_movement,
       flag,
       previous_movement,
       next_movement,
       (next_movement - current_movement) DAY TO SECOND AS diff,
       check_status
FROM   grouped_data
ORDER BY emp_id, current_movement

db<>fiddle here

CodePudding user response:

You can use a PIPELINED function:

CREATE TYPE my_table_type AS OBJECT (
  emp_id            NUMBER,
  current_movement  DATE,
  flag              NUMBER,
  previous_movement DATE,
  next_movement     DATE,
  diff              INTERVAL DAY TO SECOND,
  check_status      VARCHAR2(15)
);

CREATE TYPE my_table_tbltype AS TABLE OF my_table_type;

Then:

CREATE FUNCTION parse_my_table RETURN my_table_tbltype PIPELINED
AS
  c_shift_length      CONSTANT INTERVAL DAY TO SECOND := INTERVAL '12' HOUR;
  v_emp_id            MY_TABLE.EMP_ID%TYPE;
  v_first_movement    MY_TABLE.TIMEINOUT%TYPE;
  v_previous_movement MY_TABLE.TIMEINOUT%TYPE;
  v_next_movement     MY_TABLE.TIMEINOUT%TYPE;
  v_check_status      VARCHAR2(15);
BEGIN
  FOR row IN (
    SELECT m.*,
           ROW_NUMBER() OVER (PARTITION BY emp_id ORDER BY timeinout) AS rn,
           LAG(timeinout) OVER (PARTITION BY emp_id ORDER BY timeinout)
             AS previous_movement,
           LEAD(timeinout) OVER (PARTITION BY emp_id ORDER BY timeinout)
             AS next_movement
    FROM   my_table m
    ORDER BY m.emp_id, m.timeinout
  )
  LOOP
    IF v_emp_id IS NULL
    OR v_emp_id != row.emp_id
    OR row.timeinout >= v_first_movement   c_shift_length THEN
      v_emp_id            := row.emp_id;
      v_first_movement    := row.timeinout;
      v_previous_movement := NULL;
      v_next_movement     := row.next_movement;
      v_check_status      := 'FIRST_CHECK';
    ELSIF row.next_movement >= v_first_movement   c_shift_length 
    OR    row.next_movement IS NULL THEN
      v_previous_movement := row.previous_movement;
      v_next_movement     := NULL;
      v_check_status      := 'LAST_CHECK';
    ELSE
      v_previous_movement := row.previous_movement;
      v_next_movement     := row.next_movement;
      v_check_status      := 'DURING_DAY';
    END IF;
    PIPE ROW (
      my_table_type(
        row.emp_id,
        row.timeinout,
        row.flag,
        v_previous_movement,
        v_next_movement,
        (v_next_movement - row.timeinout) DAY TO SECOND,
        v_check_status
      )
    );
  END LOOP;
END;
/

Then:

SELECT *
FROM   TABLE(parse_my_table());

Outputs:

EMP_ID CURRENT_MOVEMENT FLAG PREVIOUS_MOVEMENT NEXT_MOVEMENT DIFF CHECK_STATUS
1 2018-07-03T16:39:44 0 2018-07-04T01:14:40 00 08:34:56.000000 FIRST_CHECK
1 2018-07-04T01:14:40 1 2018-07-03T16:39:44 2018-07-04T01:14:44 00 00:00:04.000000 DURING_DAY
1 2018-07-04T01:14:44 1 2018-07-04T01:14:40 LAST_CHECK
1 2018-07-04T16:14:52 0 2018-07-05T01:07:40 00 08:52:48.000000 FIRST_CHECK
1 2018-07-05T01:07:40 1 2018-07-04T16:14:52 2018-07-05T01:07:44 00 00:00:04.000000 DURING_DAY
1 2018-07-05T01:07:44 1 2018-07-05T01:07:40 LAST_CHECK
1 2018-07-05T16:31:08 0 2018-07-06T01:01:48 00 08:30:40.000000 FIRST_CHECK
1 2018-07-06T01:01:48 1 2018-07-05T16:31:08 2018-07-06T01:01:52 00 00:00:04.000000 DURING_DAY
1 2018-07-06T01:01:52 1 2018-07-06T01:01:48 LAST_CHECK
2 2018-07-03T08:37:40 0 2018-07-03T16:27:36 00 07:49:56.000000 FIRST_CHECK
2 2018-07-03T16:27:36 0 2018-07-03T08:37:40 LAST_CHECK
2 2018-07-04T08:37:04 0 2018-07-04T12:58:36 00 04:21:32.000000 FIRST_CHECK
2 2018-07-04T12:58:36 0 2018-07-04T08:37:04 2018-07-04T13:09:48 00 00:11:12.000000 DURING_DAY
2 2018-07-04T13:09:48 0 2018-07-04T12:58:36 2018-07-04T17:15:32 00 04:05:44.000000 DURING_DAY
2 2018-07-04T17:15:32 0 2018-07-04T13:09:48 LAST_CHECK

db<>fiddle here

  • Related