2

So, next problem :'), I have the following query that @MatBailie provided to me here (thanks again!):

SELECT
  taskname,
  employee,
  SUM(
    DATEDIFF(
      LEAST(     enddate, '2023-12-31'),
      GREATEST(startdate, '2023-01-01')
    )
    +1
  ) AS total_days,
FROM
  schedule
WHERE
  startDate <= '2023-12-31'
  AND
  endDate   >= '2023-01-01'
GROUP BY
  employee,
  taskname

This query will tell me how many days a certain employee has spent on a certain task in a given period of time, and it works great!

The next thing I would like to do however, is to substract non-working days from the SUM of DATEDIFFs for some of the tasks (e.g. when the task has "count_non_working_days= 0" in a reference table called 'activities').

For example, my schedule also keeps track of the amount of days off every employee has taken (days off are also scheduled as tasks). But of course, days off that fall in a weekend or on a holiday should not be counted towards the total of days off a person has taken in a year. (Note that I did consider scheduling days off only on weekdays/non-holidays, but this is not a practical option in the scheduling software I use because employees request a leave from date A to date B, and this request is approved or denied as-is (they don't make 3 holiday requests excluding the weekends if they want to go on a vacation for 3 weeks, if you get my drift).

So, if an employee goes on a vacation for 10 days, this is counted as 10 days off, but this holiday may have 1 or 2 weekends in it, so the sum of days of that the employee has taken off should be 6, 7 or 8, and not 10. Furthermore, if it has a holiday such as Easter Monday in it (I have all dates of my holidays in a PHP array), this should also be subtracted.

I have tried the solutions mentioned here, but I couldn't get them to work (a) because those are in SQL server and (b) because they don't allow putting in an array of holidays, (c) nor allow toggling the subtraction on and off depending on the event type.

Here's my attempt of explaining what I'm trying to do in my pseudo-SQL:

SELECT
  taskname,
  employee,
  IF( activities.count_non_working_days=1,  
    -- Just count the days that fall in the current year: 
    SUM(
      DATEDIFF(
        LEAST(      enddate, '2023-12-31'),
        GREATEST( startdate, '2023-01-01')
      ) 
      + 1
    ) AS total_days,
    -- Subtract the amount of saturdays, sundays and holidays:
    SUM(
      DATEDIFF(
        LEAST(      enddate, '2023-12-31'),
        GREATEST( startdate, '2023-01-01')
      ) 
      - [some way of getting the amount of saturdays, sundays and holidays that fall within this date range]
      + 1
    ) AS total_days
  )
FROM
  schedule
LEFT JOIN
  activities
  ON activity.name = schedule.name
WHERE
  startDate <= '2023-12-31'
  AND
  endDate   >= '2023-01-01'
GROUP BY
  employee,
  taskname

I know the query above is probably faulty on so many levels, but I hope it clarifies what I'm trying to do.

Thanks once more for all the help!

Edit: basically I need something like this, but in MySQL and preferably with a toggle that turns the subtraction on or off depending on the task type.

Edit 2: To clarify: my schedule table holds ALL activities, including holidays. For example, some records may include:

employee taskname startDate endDate
Mr. Anderson Programming 2023-01-02 2023-01-06
Mr. Anderson Programming 2023-01-09 2023-01-14
Mr. Anderson Vacation 2023-01-14 2023-01-31

In another table, Programming is defined as "count_non_working_days=1", because working in the weekends should count, while Vacation is defined as "count_non_working_days=0", because taking a day off on the weekend should not count towards your total amount of days taken off.

The totals for this month should therefore state that:

Mr. Anderson has done Programming for 11 days (of which 1 was on a saturday)

Mr. Anderson has taken 12 days off for (because the 2 weekends in this period don't count as days off).

Laurens Swart
  • 1,234
  • 9
  • 24
  • I like where you're going with this, and I can definitely create such a table. How would I 'join' on said table to have only these is_working_day=1 days included in the DATEDIFF counts? – Laurens Swart Jan 10 '23 at 09:45
  • I think I may have misunderstood some of the details, which is why it's ***really*** important to include example data and desired results. Does `schedule` include certain tasks and `activities` include booked holiday ranges? – MatBailie Jan 10 '23 at 10:01
  • Or, are schedule and activities actually the same table, but differentiated based on a column value? – MatBailie Jan 10 '23 at 10:08
  • And, is your next question how to deal with dates and TIMES, rather than whole dates? *(In which case you **really** want to move to exclusive enddates, rather than inclusive enddates, if at all possible)* – MatBailie Jan 10 '23 at 10:09
  • [Why should I provide a Minimal Reproducible Example for a very simple SQL query?](https://meta.stackoverflow.com/q/333952) – MatBailie Jan 10 '23 at 10:18
  • Sorry, to explain: my schedule table holds ALL activities, including holidays. I'll update my question. – Laurens Swart Jan 10 '23 at 10:57
  • I have updated my question with a sample table and expected result. – Laurens Swart Jan 10 '23 at 11:04
  • So, an employee won't be on multiple tasks at the same time? No need to account for things like `Programming for 10 days, but for two of those days they were on holiday, so result = 8`? – MatBailie Jan 10 '23 at 11:31

2 Answers2

3

Create a calendar table, with every date of interest (so, something like 2000-01-01 to 2099-01-01) and include columns such as is_working_day which can be set to TRUE/FLASE or 1/0. Then you can update that column as necessary, and join on that table in your query to get working dates that the employee has booked off.

In short, you count the relevant dates, rather than deducting the irrelevant dates.

SELECT
  s.employee,
  s.taskname,
  COUNT(*)    AS total_days,
FROM
(
    schedule    AS s
  INNER JOIN
    activities  AS a
      ON  a.taskname = s.taskname
)
INNER JOIN
  calendar    AS c
    ON  c.calendar_date  >= s.startDate
    AND c.calendar_date  <= s.endDate
    AND c.is_working_day >= 1 - a.count_non_working_days 
WHERE
      c.calendar_date >= '2023-01-01'
  AND c.calendar_date <= '2023-12-31'
GROUP BY
  s.employee,
  s.taskname

Your calendar table can then also include flags such as is_weekend, is_bank_holiday, is_fubar, is_amazing, etc, and the is_working_day can be a computed column from those inputs.


Note on is_working_day filter...

   WHERE
     ( count_non_working_day = 1 AND is_working_day IN (0, 1) )
     OR
     ( count_non_working_day = 0 AND is_working_day IN (   1) )


   -- change to (1 - count_non_working_day)

   WHERE
     ( (1 - count_non_working_day) = 0 AND is_working_day IN (0, 1) )
     OR
     ( (1 - count_non_working_day) = 1 AND is_working_day IN (   1) )


   -- simplify

   WHERE
     ( (1 - count_non_working_day) <= is_working_day )
     OR
     ( (1 - count_non_working_day) <= is_working_day )
  

   -- simplify

   WHERE
     ( (1 - count_non_working_day) <= is_working_day )

Demo: https://dbfiddle.uk/YAmpLmVE

MatBailie
  • 83,401
  • 18
  • 103
  • 137
  • (Sorry, had a typo in the SQL of my prevous comment). Thank you so much! It's working!! – Laurens Swart Jan 10 '23 at 11:09
  • Can I buy you a coffee/beer?? – Laurens Swart Jan 10 '23 at 11:14
  • Edit: disregard my last (removed) comment, it DOES actually count those days as well... not sure why I'm still getting a few incorrect numbers (while most are correct) but I'll get back to you on that! Thanks again!! – Laurens Swart Jan 10 '23 at 11:28
  • OK found out the problem: it's counting every task 1 day short, except tasks that last 1 day... (in the previous version of the code we added +1 to all the DATEDIFFs remember?) – Laurens Swart Jan 10 '23 at 12:35
  • Thanks for adding the part on the count_non_working_day! Could you please explain where to add this in the existing query (assuming the count_non_working_day is defined in a table called activities which also holds the tasknames)? – Laurens Swart Jan 10 '23 at 12:40
  • Managed to get the count_non_working_day toggle working with another INNER JOIN from the activities table and then using your simplified WHERE clause. Thank you very much. Any chance you could help on why it's counting the duration of tasks that last multiple days 1 day short? I.e. a task that last 5 days is counted as 4 days. – Laurens Swart Jan 10 '23 at 12:48
  • @LaurensSwart - You need to make a minimal reproducible example. Create demo/test/example tables and populate them with the minimal amount of data to reproduce your problem, and add that data to your question. – MatBailie Jan 10 '23 at 13:05
  • @LaurensSwart - https://dbfiddle.uk/YAmpLmVE – MatBailie Jan 10 '23 at 13:19
  • Thank you! Managed to reproduce the problem in your example, because in reality, my startDate and endDate columns are DATETIME fields, and all tasks have a starting and ending time as well. This apparently causes the d.date >= s.startDate to be false on the first day (I presume because d.date >= s.startDate returns false when s.startDate has a time larger than 00:00:00). See example: https://dbfiddle.uk/6qdudZsr . You can see here that Programming returns 17 in stead of 19, because the two first days are not being counted. When I set the starting times to 00:00:00, these two days are counted. – Laurens Swart Jan 10 '23 at 14:16
  • UPDATE: managed to get it working by wrapping the DATETIME fields in DATE(): https://dbfiddle.uk/ihrrv5qt . Now let me buy you a beer/coffee! – Laurens Swart Jan 10 '23 at 14:21
  • @LaurensSwart Pay it forward, not back ;) Find someone that needs a coffee or a beer and buy Them one, courtesy of SO. – MatBailie Jan 10 '23 at 14:27
0

This is to calculate all the weeekends between two giving dates It may help you :

SELECT (
    ((WEEK('2022-12-31') - WEEK('2022-01-01')) * 2) -
    (case when weekday('2022-12-31') = 6 then 1 else 0 end) -
    (case when weekday('2022-01-01') = 5 then 1 else 0 end)
)

You will have to substract also holidays that fall within this date range.

SelVazi
  • 10,028
  • 2
  • 13
  • 29