计算两个日期之间的时间差,不包括周末,以天时分秒的格式。

huangapple go评论81阅读模式
英文:

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

问题

以下是你要翻译的代码部分:

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;

希望这能帮助你解决问题。

英文:

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.

&lt;?php
require &#39;vendor\carbon\autoload.php&#39;;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterval;


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

while ($step &lt; $firstResponse) {
    if ($step-&gt;isWeekend()) {
        $step = $step-&gt;next(&#39;Monday&#39;);

        continue;
    }

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

    $diffInSeconds += $step-&gt;diffInSeconds($nextStep);
    $step = $nextStep;
}

echo CarbonInterval::seconds($diffInSeconds)-&gt;cascade()-&gt;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 &#39;2023-01-02 10:34:36&#39;::timestamp AS date
  UNION ALL
  SELECT CASE
           WHEN EXTRACT(ISODOW FROM date) IN (6, 7) THEN date + INTERVAL &#39;1 day&#39;*(8-EXTRACT(ISODOW FROM date))
           ELSE date + INTERVAL &#39;1 DAY&#39;
         END
  FROM date_range
  WHERE date + INTERVAL &#39;1 DAY&#39; &lt; &#39;2023-01-10 15:12:24&#39;::timestamp
)
SELECT
  CONCAT(
    FLOOR(diff / 86400), &#39; days &#39;,
    FLOOR((diff % 86400) / 3600), &#39; hours &#39;,
    FLOOR((diff % 3600) / 60), &#39; minutes &#39;,
    FLOOR(diff % 60), &#39; seconds&#39;
  ) AS duration
FROM (
  SELECT
    EXTRACT(EPOCH FROM (&#39;2023-01-10 15:12:24&#39;::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

答案1

得分: 1

以下是代码的翻译部分:

"Really interesting question which completely distracted me from work! This question is similar and could be useful to you:https://stackoverflow.com/questions/23290454/get-all-dates-between-two-dates-in-sql-server .
I think the recommendation of building a calendar table would help you a lot!

To directly answer your question I think it you need to change from max to min, as I have, here:

FROM (
SELECT
EXTRACT(EPOCH FROM ('2023-01-10 15:12:24'::timestamp - MIN(date))::interval) AS diff
FROM date_range) t;

However even with this you return 8 days when I think the answer should be 7....

I'm not overly familiar with the recursive so I went for something like the below. The case statement can definitely be optimised. {note I'm snowflake dialect}

SET start_date = '2023-01-02 10:34:36';
SET end_date = '2023-01-10 15:12:24';
WITH DateRange(DateData) AS
(
SELECT $start_date::DATe as Date
UNION ALL
SELECT DATEADD(DAYS ,1,DateData)::DATE
FROM DateRange
WHERE DateData < $end_date
)
,date_array AS (
SELECT datedata
, DAYOFWEEK(datedata) AS day_index
, dayname(datedata) AS day_name
, $start_date
, $end_date
FROM daterange
)

SELECT
$start_date
, substring($start_date ,12,8)::TIME as start_time
, $end_date
, substring($end_date ,12,8)::TIME as end_time
, HOUR(end_time) - HOUR(start_time) as hours
, MINUTE(end_time) - MINUTE(start_time) as minutes
, SECOND(end_time) - SECOND(start_time) as seconds
, COUNT(DISTINCT CASE WHEN day_index NOT IN (6,0) THEN datedata END ) as days
, CONCAT (
CASE WHEN hours < 0 THEN days -1 ELSE days END , ' days ' ,

CASE
WHEN minutes < 0 AND hours < 0 THEN 24 + hours -1
WHEN minutes > 0 AND hours < 0 THEN 24 + hours
WHEN minutes < 0 AND hours > 0 THEN hours -1
ELSE hours END
, ' hours ' ,
CASE
WHEN seconds < 0 AND minutes < 0 THEN 60 + minutes -1
WHEN seconds > 0 AND minutes < 0 THEN 60 + minutes
WHEN seconds < 0 AND minutes > 0 THEN minutes -1
ELSE minutes END

, ' minutes ' ,
CASE WHEN seconds <0 THEN 60 + seconds ELSE seconds END , ' seconds') as output
FROM date_array
GROUP BY 1,2,3"

请注意,代码中的HTML实体(如&lt;)未被正确翻译,因为这些实体没有明确的中文翻译。

英文:

Really interesting question which completely distracted me from work! This question is similar and could be useful to you:https://stackoverflow.com/questions/23290454/get-all-dates-between-two-dates-in-sql-server .
I think the recommendation of building a calendar table would help you a lot!

To directly answer your question I think it you need to change from max to min, as I have, here:

FROM (
SELECT
EXTRACT(EPOCH FROM (&#39;2023-01-10 15:12:24&#39;::timestamp - MIN(date))::interval) AS diff
  FROM date_range) t;

However even with this you return 8 days when I think the answer should be 7....

I'm not overly familiar with the recursive so I went for something like the below. The case statement can definitely be optimised. {note I'm snowflake dialect}

SET start_date = &#39;2023-01-02 10:34:36&#39;;
SET end_date = &#39;2023-01-10 15:12:24&#39;;
WITH DateRange(DateData) AS
    (
    SELECT $start_date::DATe as Date
    UNION ALL
    SELECT DATEADD(DAYS ,1,DateData)::DATE
    FROM DateRange
    WHERE DateData &lt; $end_date
    )
,date_array AS (
    SELECT datedata
            , DAYOFWEEK(datedata) AS day_index
            , dayname(datedata) AS day_name
            , $start_date
            , $end_date
    FROM daterange
)

SELECT
       $start_date
       , substring($start_date ,12,8)::TIME as start_time
       , $end_date
       , substring($end_date ,12,8)::TIME as end_time
       , HOUR(end_time) - HOUR(start_time) as hours
       , MINUTE(end_time) - MINUTE(start_time) as minutes
       , SECOND(end_time) - SECOND(start_time) as seconds
       , COUNT(DISTINCT CASE WHEN day_index NOT IN (6,0) THEN  datedata END ) as  days
       , CONCAT (
           CASE WHEN hours &lt; 0 THEN days -1 ELSE days END , &#39; days &#39; ,

           CASE
               WHEN minutes &lt; 0 AND hours &lt; 0 THEN 24 + hours  -1
               WHEN minutes &gt; 0 AND hours &lt; 0 THEN 24 + hours
               WHEN minutes &lt; 0 AND hours &gt; 0 THEN   hours -1
                   ELSE hours END
           , &#39; hours &#39; ,
           CASE
               WHEN seconds &lt; 0 AND minutes &lt; 0 THEN 60 + minutes  -1
               WHEN seconds &gt; 0 AND minutes &lt; 0 THEN 60 + minutes
               WHEN seconds &lt; 0 AND minutes &gt; 0 THEN   minutes -1
                   ELSE minutes END

           , &#39; minutes &#39; ,
           CASE WHEN seconds &lt;0 THEN 60 + seconds ELSE seconds END , &#39; seconds&#39;) as output
FROM date_array
GROUP BY 1,2,3

Output :


| output |

| 7 days 4 hours 37 minutes 48 seconds |

答案2

得分: 1

以下是翻译好的部分:

  1. "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." -> "你总是得到0天的原因是因为你选择了MAX(date),结果是2023-01-10 10:34:36(满足退出条件的第一个值),与2023-01-10 15:12:24相差0天。"

  2. "Perhaps you should select MIN(date)." -> "也许你应该选择MIN(date)。"

  3. "I am not even sure that is valid for all timestamps not if the start and/or end dates specified fall on a weekend?" -> "我甚至不确定这是否适用于所有时间戳,如果指定的起始日期和/或结束日期落在周末怎么办?"

  4. "But why are you messing around with epoch from an interval then the "complicated" date/time calculations." -> "但为什么你要处理来自一个间隔的'epoch',然后进行'复杂'的日期/时间计算。"

  5. "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:" -> "你的过程围绕着两个硬编码的时间戳。两个时间戳的减法会产生一个'interval',然后你可以直接提取每个字段。你的查询简化为:"

  6. "You can even wrap the query in a SQL function and completely hide it away." -> "你甚至可以将查询包装在一个SQL函数中,并完全隐藏它。"

  7. "create or replace function diff_without_weekend( start_date_in timestamp, end_date_in timestamp) returns text language sql as $$" -> "创建或替换函数diff_without_weekend( start_date_in timestamp, end_date_in timestamp) 返回文本 语言sql 如下:$$"

  8. "with weekend_days (wkend) as..." -> "使用weekend_days(wkend)..."

  9. "1: https://dbfiddle.uk/SsNKfkmR" -> "1: https://dbfiddle.uk/SsNKfkmR"

英文:

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? <br/>
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 &#39;2023-01-02 10:34:36&#39;::timestamp          --- as parameter $1
              , &#39;2023-01-10 15:12:24&#39;::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 &#39;1 day&#39;) dn(d)
       ) 
select concat( extract( day from diff)     , &#39; days &#39;     
             , extract( hours from diff)   , &#39; hours &#39;   
             , extract( minutes from diff) , &#39; minuets &#39;  
             , extract( seconds from diff)::int , &#39; seconds &#39;
             )
   from ( 
          select (end_date-start_date)- ( wkend * interval &#39;1 day&#39;) 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 &#39;1 day&#39;) dn(d)
          ) 
    select CONCAT( extract( day from diff)     , &#39; days &#39;     
                 , extract( hours from diff)   , &#39; hours &#39;   
                 , extract( minutes from diff) , &#39; minuets &#39;  
                 , extract( seconds from diff)::int , &#39; seconds &#39;)
      from ( select (end_date_in -start_date_in )- ( wkend * interval &#39;1 day&#39;) diff
               from weekend_days
           ) sq;
$$;

答案3

得分: 1

由于您的时间差是通过从“结束日期”提取MAX(date)来计算的,其值与“结束日期”相同日期或“结束日期”之前的最后一个工作日相同,因此无法正确计算差异天数的值。

您可以通过以下查询来实现您想要的结果:

  1. 使用generate_series函数从“开始日期”生成日期系列,然后仅获取该系列中的工作日。
  2. 对日期系列中的每个工作日期,计算每个日期的“工作时间”(“工作时间”的数据类型将为Postgres的interval)。
  3. 然后计算日期系列中所有工作日期的总“工作时间”。
SET intervalstyle = 'postgres_verbose'; -- 间隔的显示样式设置

WITH date_range AS 
      (SELECT '2023-01-02 10:34:36'::timestamp AS start_date
            , '2023-01-10 15:12:24'::timestamp AS end_date),
     date_series AS
      (SELECT  d as date
            ,  (CASE
                  WHEN start_date::date = d THEN interval '1 day' + (d - start_date)::interval
                  WHEN end_date::date = d THEN  (end_date - d)::interval
                  ELSE interval '1 day'
               END) AS working_time
       FROM date_range 
            CROSS JOIN generate_series(start_date::date, end_date::timestamp, interval '1 day') d
       WHERE EXTRACT (ISODOW FROM  d) BETWEEN 1 AND 5)
       
SELECT SUM(working_time) AS working_time
FROM date_series;

查看演示链接

英文:

Since your difference time is calculated by extract end date to the MAX(date) - which its value is same date as end date or the last working date before end date, so difference days value could not be calculated correctly.

You could achieve your desired result by using below query.

  1. Generating date series from start date to end date by using generate_series function, then get only working days from this series.
  2. For each working date in date series, calculate working time per each date (data type of working time will be Postgres's interval).
  3. Then calculating total working time of all working dates in date series.
SET intervalstyle = &#39;postgres_verbose&#39;; -- format display style for interval

WITH date_range AS 
      (SELECT &#39;2023-01-02 10:34:36&#39;::timestamp AS start_date
            , &#39;2023-01-10 15:12:24&#39;::timestamp AS end_date),
     date_series AS
      (SELECT  d as date
            ,  (CASE
                  WHEN start_date::date = d THEN interval &#39;1 day&#39; + (d - start_date)::interval
                  WHEN end_date::date = d THEN  (end_date - d)::interval
                  ELSE interval &#39;1 day&#39;
               END) AS working_time
       FROM date_range 
            CROSS JOIN generate_series(start_date::date, end_date::timestamp, interval &#39;1 day&#39;) d
       WHERE EXTRACT (ISODOW FROM  d) BETWEEN 1 AND 5)
       
SELECT SUM(working_time) AS working_time
FROM date_series;

See demo here.

huangapple
  • 本文由 发表于 2023年3月3日 21:23:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/75627636.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定