Search code examples
sqlpostgresqlphp-carbon

Calculate time difference bewteen 2 datetime excluding weekend in days hours minutes seconds format


I want to calculate the time difference between two datetime. Saturday and sunday need to be excluded from the calculation.

For exemple difference between 2023-01-10 15:12:24 and 2023-01-01 10:34:36 is 6 days 4 hours 37 minutes 48 seconds according to PHP carbon.

<?php
require 'vendor\carbon\autoload.php';
use Carbon\CarbonImmutable;
use Carbon\CarbonInterval;


$created = CarbonImmutable::parse("2023-01-02 10:34:36");
$firstResponse = CarbonImmutable::parse("2023-01-10 15:12:24");
$diffInSeconds = 0;
$step = $created;

while ($step < $firstResponse) {
    if ($step->isWeekend()) {
        $step = $step->next('Monday');

        continue;
    }

    $nextStep = min($firstResponse, $step->addDay()->startOfDay());

    $diffInSeconds += $step->diffInSeconds($nextStep);
    $step = $nextStep;
}

echo CarbonInterval::seconds($diffInSeconds)->cascade()->forHumans(); //6 days 4 hours 37 minutes 48 seconds

The goal is to calculate this value using SQL.

I've come to this following query :

WITH RECURSIVE date_range AS (
  SELECT '2023-01-02 10:34:36'::timestamp AS date
  UNION ALL
  SELECT CASE
           WHEN EXTRACT(ISODOW FROM date) IN (6, 7) THEN date + INTERVAL '1 day'*(8-EXTRACT(ISODOW FROM date))
           ELSE date + INTERVAL '1 DAY'
         END
  FROM date_range
  WHERE date + INTERVAL '1 DAY' < '2023-01-10 15:12:24'::timestamp
)
SELECT
  CONCAT(
    FLOOR(diff / 86400), ' days ',
    FLOOR((diff % 86400) / 3600), ' hours ',
    FLOOR((diff % 3600) / 60), ' minutes ',
    FLOOR(diff % 60), ' seconds'
  ) AS duration
FROM (
  SELECT
    EXTRACT(EPOCH FROM ('2023-01-10 15:12:24'::timestamp - MAX(date))::interval) AS diff
  FROM date_range
) t;

Output :

----------------------------------------
| duration                             |
----------------------------------------
| 0 days 4 hours 37 minutes 48 seconds |
----------------------------------------

I don't understand why days has value equal to 0.

How can I fix the days value ?

Fiddle : https://www.db-fiddle.com/f/3V6QVdE1PPETKS6yN33zdE/0


Solution

  • The reason you always get 0 days is because you selecting MAX(date) which turns out to be 2023-01-10 10:34:36 (the first value that satisfies your exit condition) which is 0 days from 2023-01-10 15:12:24. Perhaps you should select MIN(date). I am not even sure that is valid for all timestamps not if the start and/or end dates specified fall on a weekend?
    But why are you messing around with epoch from an interval then the "complicated" date/time calculations. Your process centers around 2 hard-coded timestamps. The subtraction of 2 timestamps gives an interval then you can directly extract each field. Your query reduces to: (see demo)

    with parms (start_date, end_date) as
           ( select '2023-01-02 10:34:36'::timestamp          --- as parameter $1
                  , '2023-01-10 15:12:24'::timestamp          --- as parameter $2
           ) 
         , weekend_days (wkend) as 
           ( select sum(case when extract(isodow from d) in (6, 7) then 1 else 0 end) 
               from parms 
               cross join generate_series(start_date, end_date, interval '1 day') dn(d)
           ) 
    select concat( extract( day from diff)     , ' days '     
                 , extract( hours from diff)   , ' hours '   
                 , extract( minutes from diff) , ' minuets '  
                 , extract( seconds from diff)::int , ' seconds '
                 )
       from ( 
              select (end_date-start_date)- ( wkend * interval '1 day') diff
                from parms 
                join weekend_days on true
             ) sq;
    

    You can even wrap the query in a SQL function and completely hide it away.

    create or replace function diff_without_weekend( start_date_in timestamp
                                                   , end_date_in timestamp)
      returns text 
     language sql
     as $$
         with weekend_days (wkend) as 
              ( select sum(case when extract(isodow from d) in (6, 7) then 1 else 0 end) 
                  from generate_series(start_date_in, end_date_in, interval '1 day') dn(d)
              ) 
        select CONCAT( extract( day from diff)     , ' days '     
                     , extract( hours from diff)   , ' hours '   
                     , extract( minutes from diff) , ' minuets '  
                     , extract( seconds from diff)::int , ' seconds ')
          from ( select (end_date_in -start_date_in )- ( wkend * interval '1 day') diff
                   from weekend_days
               ) sq;
    $$;