Repeating calendar events and some final maths - mysql

I am trying to have a go at the infamous repeating events on calendars using PHP/MySQL. I've finally found something that seems to work. I found my answer here but I'm having a little difficulty finishing it off.
My first table 'events'.
ID NAME
1 Sample Event
2 Another Event
My second table 'events_meta that stores the repeating data.
ID event_id meta_key meta_value
1 1 repeat_start 1336312800 /* May 7th 2012 */
2 1 repeat_interval_1 432000 /* 5 days */
With repeat_start being a date with no time as a unix timestamp, and repeat_interval an amount in seconds between intervals (432000 is 5 days).
I then have the following MySQL which I modified slightly from the above link. The timestamp used below (1299132000 which is 12th May 2012) is the current day with no time.
SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN `events_meta` EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
AND (
( CASE ( 1336744800 - EM1.`meta_value` )
WHEN 0
THEN 1
ELSE ( 1336744800 - EM1.`meta_value` ) / EM2.`meta_value`
END
)
) = 1
In the above MySQL, the following code deducts the repeat_start field (EM1.'meta_value') from the current date and then divides it by the repeat interval field (EM2.'meta_value').
ELSE ( 1336744800 - EM1.`meta_value` ) / EM2.`meta_value`
OR
TODAYS DATE - START DATE / 5 DAYS
So here's the maths:
1336744800 - 1336312800 = 432000
432000 / 432000 = 1
Now that works perfect. But if I change the current timestamp 5 days ahead to 1336312800 which is 17th Mat 2012, it looks a bit like this:
1336312800 - 1336312800 = 864000
86400 / 432000 = 2
Which doesn't work because it equals 2 and in the MySQL it needs to equal 1. So I guess my question is, how do I get the MySQL to recognise a whole number rather than having to do this?
...
WHERE EM1.meta_key = 'repeat_start'
AND (
( CASE ( 1336744800 - EM1.`meta_value` )
WHEN 0
THEN 1
ELSE ( 1336744800 - EM1.`meta_value` ) / EM2.`meta_value`
END
)
) = IN (1,2,3,4,5,6,7,8,....)
Hope I'm making sense and I hope it's just a simple maths thing or a function that MySQL has that will help :) Thanks for your help!
EDIT: THE ANSWER
Thanks to #eggypal below, I found my answer and of course it was simple!
SELECT EV.*
FROM elvanto_calendars_events AS EV
RIGHT JOIN elvanto_calendars_events_meta AS EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN elvanto_calendars_events_meta AS EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
AND ( ( 1336744800 - EM1.`meta_value` ) % EM2.`meta_value`) = 0

It's not entirely clear what you want your query to do, but the jist of your question makes me lean toward suggesting that you look into modular arithmetic: in SQL, a % b returns the remainder when a is divided by b - if there is no remainder (i.e. a % b = 0), then a must be an exact multiple of b.
In your case, I think you're trying to find events where the time between the event start and some given literal is an exact multiple of the event interval: that is, (literal - event_start) % event_interval = 0. If it's non-zero, the value is the time to the next occurrence after literal (and, therefore, to determine whether that next occurrence occurs within some period of time, say a day, one would test to see if the remainder is less than such constant e.g. (literal - event_start) % event_interval < 86400).
If this isn't what you're after, please clarify exactly what your query is trying to achieve.

set #dat_ini = '2023-05-20',#dat_fim = '2022-11-20'; select (DATEDIFF( #dat_fim,#dat_ini )) % 60
THIS < 10
It only works for a short period.
To do this, take the start date and change the Month that is on the screen and add a year, then subtract it from the start date, then it works.

Related

MySQL - Way to simplify repetitive calculation sub-queries

I've been tasked with making output that fetches number of days passed between an order and its shipment, like this:
order_date
orders
Days0
Days1
Days7Plus
2022-11-01
12
9
3
1
2022-11-15
22
20
0
2
2022-12-02
77
65
5
7
I'm sure you can imagine example underlying data, where there's an orders table with a unique ID per record, an order date that can share multiple IDs, and each order has its own ship date.
The hard part is counting only business days, which required subtracting weekends and holidays from the date range days. I got that all figured out but it required copy-pasting these ugly sub-queries 7 more times :/ While this can be dynamically generated in other code, I figured there must be a cleaner way, since other people (some non-devs) may be testing or reviewing this, and I'll probably get grief about it.
Here's the query essentially:
# get orders shipped count
SELECT
...
# orders that were shipped x number of days from receipt
SUM(COALESCE(DATEDIFF(shipped, ordered), 0)
- ( # subtract weekend days
5 * (DATEDIFF('2022-12-05', '2022-11-01') DIV 7)
+ MID('0123444401233334012222340111123400012345001234550',
7 * WEEKDAY('2022-11-01') + WEEKDAY('2022-12-05') + 1, 1
)
)
- ( # subtract holidays
SELECT COUNT(`date`) FROM holiday WHERE active = 1
AND `date` BETWEEN '2022-11-01' AND '2022-12-05'
AND DAYOFWEEK(`date`) < 6
)
= 0) AS 0Days, # subsequently 1Days, 2Days, 3Days, etc
...
SUM(COALESCE(DATEDIFF(shipped, ordered), 0)
- ( # subtract weekend days
5 * (DATEDIFF('2022-12-05', '2022-11-01') DIV 7)
+ MID('0123444401233334012222340111123400012345001234550',
7 * WEEKDAY('2022-11-01') + WEEKDAY('2022-12-05') + 1, 1
)
)
- ( # subtract holidays
SELECT COUNT(`date`) FROM holiday WHERE active = 1
AND `date` BETWEEN '2022-11-01' AND '2022-12-05'
AND DAYOFWEEK(`date`) < 6
)
>= 7) AS Days7Plus
FROM orders
WHERE
AND ordered BETWEEN :startDate AND :endDate
GROUP BY CAST(ordered AS DATE)
ORDER BY ordered
I got the MID calculation from https://stackoverflow.com/a/6762805/14744970
I feel pretty proud of getting it all together, but I feel like I'm a small step away from collapsing the redundancy down somehow that I'm not quite understanding.
Note that I don't know if the GROUP BY actually matters with any sort of simplifying of the redundant statements.

TSQL - Calculate time between punches but exclude break time

Not sure if this is a unique question or not.
I'm needing to get calculated punch in and punch out times for a labor tracking system. Our crew has breaks from 10-10:20 AM and from 1-1:20 PM.
What I need to figure out is how to subtract this time from a total if they are still punched in during these breaks.
For example, if Joe punches in to a job at 09:53 and punches out at 10:23, I want it to show 10 minutes instead of 30.
How could I do this for few "blackout" times of 10-10:20, 1-1:20, 11-11:20, and 5-5:20?
This return the total of work minutes. This check if the worker time overlap with each break time and then change it to indicate how much overlap was.
Then calculate the total of minutes in break and finally subtract for the total of time worker punch in_out.
SQL DEMO
WITH time_off as (
SELECT * ,
CASE WHEN w.in_w < b.out_b AND w.out_w > b.in_b
THEN 'overlap'
END as overlap,
CASE WHEN w.in_w < b.in_b
THEN b.in_b
ELSE w.in_w
END as break_start,
CASE WHEN w.out_w > b.out_b
THEN b.out_b
ELSE w.out_w
END as break_end
FROM workers w
CROSS JOIN breaks b
), break_total as (
SELECT worker_id, in_w, out_w, SUM (CASE WHEN overlap = 'overlap'
THEN datediff(minute, break_start,break_end)
ELSE 0
END) as break_total
FROM time_off
GROUP BY worker_id, in_w, out_w
)
SELECT worker_id,
datediff(minute, in_w, out_w) - break_total as total_minutes
FROM break_total
For some debug do:
SELECT * FROM time_off;
SELECT * FROM break_total;

Time difference using MySQL - end time after mid night not working

I am trying to figure out whether my current time falls between two given times. It is working fine, unless if my end time falls after the mid night, while my start time is during the day.
For example, if I have:
Start: 17:00
End: 03:00
Current: 18:30
It then will not show up.
I do understand where the logic is going wrong, but I can't find a fix for it, logically and conceptually. I mean, you could claim that 06:30 PM does fall between 05 PM and 03 AM, but then 03 AM of the next day!
How do we overcome that problem?
Here is my query below:
SELECT * FROM products
JOIN restaurants
ON (products.Venue = restaurants.name)
WHERE products.Drink_Category = Beer
AND restaurants.Area = Racks
AND (
HOUR(18:30) >= HOUR(products.Start) -- here is where the time is set
AND HOUR(18:30) <= HOUR(products.End) --and here is the end
)
ORDER BY products.Price/products.Multiple ASC;
I have as well tried BETWEEN query, and yet it does not work for those specific cases.
Basically, when products.End is smaller than products.Start, just add 24 to account for the day wrapping. E.g.
SELECT * FROM products
JOIN restaurants
ON (products.Venue = restaurants.name)
WHERE products.Drink_Category = Beer
AND restaurants.Area = Racks
AND (
HOUR(18:30) >= HOUR(products.Start) -- here is where the time is set
AND HOUR(18:30) <= CASE
WHEN products.End<products.Start
THEN HOUR(products.End)+24
ELSE HOUR(products.End) END --and here is the end
)
ORDER BY products.Price/products.Multiple ASC;
you have 2 way:
1- use complete date time value in your Condition like this:
Select * FROM table_event where ((SaleDate >= #FromDate) AND (SaleDate < #ToDate)) Order by SaleDate DESC
2- you can change your condition in back end, if your end time go to tomorrow you must write another condition like this:
(HOUR(18:30) >= HOUR(products.Start) AND HOUR(23:59) <= HOUR(products.End) )
OR
( HOUR(00:00) >= HOUR(products.Start) AND HOUR(3:00) <= HOUR(products.End) )

SQL - Calculating variable moving average over variable lenghts

FIRST: This question is NOT a duplicate. I have asked this on here already and it was closed as a duplicate. While it is similar to other threads on stackoverflow, it is actually far more complex. Please read the post before assuming it is a duplicate:
I am trying to calculate variable moving averages crossover with variable dates.
That is: I want to prompt the user for 3 values and 1 option. The input is through a web front end so I can build/edit the query based on input or have multiple queries if needed.
X = 1st moving average term (N day moving average. Any number 1-N)
Y = 2nd moving average term. (N day moving average. Any number 1-N)
Z = Amount of days back from present to search for the occurance of:
option = Over/Under: (> or <. X passing over Y, or X passing Under Y)
X day moving average passing over OR under Y day moving average
within the past Z days.
My database is structured:
tbl_daily_data
id
stock_id
date
adj_close
And:
tbl_stocks
stock_id
symbol
I have a btree index on:
daily_data(stock_id, date, adj_close)
stock_id
I am stuck on this query and having a lot of trouble writing it. If the variables were fixed it would seem trivial but because X, Y, Z are all 100% independent of each other (could look, for example for 5 day moving average within the past 100 days, or 100 day moving average within the past 5) I am having a lot of trouble coding it.
Please help! :(
Edit: I've been told some more context might be helpful?
We are creating an open stock analytic system where users can perform trend analysis. I have a database containing 3500 stocks and their price histories going back to 1970.
This query will be running every day in order to find stocks that match certain criteria
for example:
10 day moving average crossing over 20 day moving average within 5
days
20 day crossing UNDER 10 day moving average within 5 days
55 day crossing UNDER 22 day moving average within 100 days
But each user may be interested in a different analysis so I cannot just store the moving average with each row, it must be calculated.
I am not sure if I fully understand the question ... but something like this might help you get where you need to go: sqlfiddle
SET #X:=5;
SET #Y:=3;
set #Z:=25;
set #option:='under';
select * from (
SELECT stock_id,
datediff(current_date(), date) days_ago,
adj_close,
(
SELECT
AVG(adj_close) AS moving_average
FROM
tbl_daily_data T2
WHERE
(
SELECT
COUNT(*)
FROM
tbl_daily_data T3
WHERE
date BETWEEN T2.date AND T1.date
) BETWEEN 1 AND #X
) move_av_1,
(
SELECT
AVG(adj_close) AS moving_average
FROM
tbl_daily_data T2
WHERE
(
SELECT
COUNT(*)
FROM
tbl_daily_data T3
WHERE
date BETWEEN T2.date AND T1.date
) BETWEEN 1 AND #Y
) move_av_2
FROM
tbl_daily_data T1
where
datediff(current_date(), date) <= #z
) x
where
case when #option ='over' and move_av_1 > move_av_2 then 1 else 0 end +
case when #option ='under' and move_av_2 > move_av_1 then 1 else 0 end > 0
order by stock_id, days_ago
Based on answer by #Tom H here: How do I calculate a moving average using MySQL?

MySQL: Average interval between records

Assume this table:
id date
----------------
1 2010-12-12
2 2010-12-13
3 2010-12-18
4 2010-12-22
5 2010-12-23
How do I find the average intervals between these dates, using MySQL queries only?
For instance, the calculation on this table will be
(
( 2010-12-13 - 2010-12-12 )
+ ( 2010-12-18 - 2010-12-13 )
+ ( 2010-12-22 - 2010-12-18 )
+ ( 2010-12-23 - 2010-12-22 )
) / 4
----------------------------------
= ( 1 DAY + 5 DAY + 4 DAY + 1 DAY ) / 4
= 2.75 DAY
Intuitively, what you are asking should be equivalent to the interval between the first and last dates, divided by the number of dates minus 1.
Let me explain more thoroughly. Imagine the dates are points on a line (+ are dates present, - are dates missing, the first date is the 12th, and I changed the last date to Dec 24th for illustration purposes):
++----+---+-+
Now, what you really want to do, is evenly space your dates out between these lines, and find how long it is between each of them:
+--+--+--+--+
To do that, you simply take the number of days between the last and first days, in this case 24 - 12 = 12, and divide it by the number of intervals you have to space out, in this case 4: 12 / 4 = 3.
With a MySQL query
SELECT DATEDIFF(MAX(dt), MIN(dt)) / (COUNT(dt) - 1) FROM a;
This works on this table (with your values it returns 2.75):
CREATE TABLE IF NOT EXISTS `a` (
`dt` date NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `a` (`dt`) VALUES
('2010-12-12'),
('2010-12-13'),
('2010-12-18'),
('2010-12-22'),
('2010-12-24');
If the ids are uniformly incremented without gaps, join the table to itself on id+1:
SELECT d.id, d.date, n.date, datediff(d.date, n.date)
FROM dates d
JOIN dates n ON(n.id = d.id + 1)
Then GROUP BY and average as needed.
If the ids are not uniform, do an inner query to assign ordered ids first.
I guess you'll also need to add a subquery to get the total number of rows.
Alternatively
Create an aggregate function that keeps track of the previous date, and a running sum and count. You'll still need to select from a subquery to force the ordering by date (actually, I'm not sure if that's guaranteed in MySQL).
Come to think of it, this is a much better way of doing it.
And Even Simpler
Just noting that Vegard's solution is much better.
The following query returns correct result
SELECT AVG(
DATEDIFF(i.date, (SELECT MAX(date)
FROM intervals WHERE date < i.date)
)
)
FROM intervals i
but it runs a dependent subquery which might be really inefficient with no index and on a larger number of rows.
You need to do self join and get differences using DATEDIFF function and get average.