简体   繁体   中英

How to find exact seasons and days from a given date range?

I need this for rent a car price calculation. Cars prices are different according to seasons.

I have a season_dates table like this

id    slug    start                  end 
1     low     2011-01-01 00:00:00    2011-04-30 00:00:00
2     mid     2011-05-01 00:00:00    2011-06-30 00:00:00
3     high    2011-07-01 00:00:00    2011-08-31 00:00:00
4     mid     2011-09-01 00:00:00    2011-10-31 00:00:00
5     low     2011-11-01 00:00:00    2011-12-31 00:00:00

Users selecting days, for example:

start_day   08/20   end_day  08/25

My query like that:

SELECT * from arac_donemler
where DATE_FORMAT(start, '%m/%d') <= '08/20'
  and DATE_FORMAT(end, '%m/%d') >= '08/25'

This gives me high season that's correct.

But what I couldn't handle is: what if user selects a date range between 2 seasons?

For example from 20 August to 05 September.

This time I have to find that date ranges belongs to which seasons? And I have to calculate how many days per each seasons?

For the example above, high season ending at 31 August. So 31-20 = 11 days for high season, 5 days for mid season.

How can I provide this separation?

I hope I could explain it.

I tried so many things like join table inside but couldn't succeed it.

I'll let others chime in with the right way to do date comparisons in SQL (yours almost certainly kills indexing for the table), but for a start, you can get exactly the seasons that are relevant by

select * from arac_donemler
 where end >= [arrival-date]
   and start <= [departure-date]

Then you should do the rest of your processing (figure out how many days in each season and so forth) in the business logic instead of in the database query.

I would store all single days within a table. This is a simple example.

create table dates (
id int not null auto_increment primary key,
pday date,
slug tinyint,
price int);

insert into dates (pday,slug,price)
values 
('2011-01-01',1,10),
('2011-01-02',1,10),
('2011-01-03',2,20),
('2011-01-04',2,20),
('2011-01-05',2,20),
('2011-01-06',3,30),
('2011-01-07',3,30),
('2011-01-08',3,30);

select 
concat(min(pday),'/',max(pday)) as period,
count(*) as days,
sum(price) as price_per_period
from dates 
where pday between '2011-01-02' and '2011-01-07'
group by slug

+-----------------------+------+------------------+
| period                | days | price_per_period |
+-----------------------+------+------------------+
| 2011-01-02/2011-01-02 |    1 |               10 |
| 2011-01-03/2011-01-05 |    3 |               60 |
| 2011-01-06/2011-01-07 |    2 |               60 |
+-----------------------+------+------------------+
3 rows in set (0.00 sec)

EDIT. Version with grandtotal

select 
case
when slug is null then 'Total' else concat(min(pday),'/',max(pday)) end as period,
count(*) as days,
sum(price) as price_per_period
from dates 
where pday between '2011-01-02' and '2011-01-07'
group by slug
with rollup;

+-----------------------+------+------------------+
| period                | days | price_per_period |
+-----------------------+------+------------------+
| 2011-01-02/2011-01-02 |    1 |               10 |
| 2011-01-03/2011-01-05 |    3 |               60 |
| 2011-01-06/2011-01-07 |    2 |               60 |
| Total                 |    6 |              130 |
+-----------------------+------+------------------+
4 rows in set (0.00 sec)

edit. Stored procedure to populate table

delimiter $$
create procedure calendario(in anno int)
begin
  declare i,ultimo int;
  declare miadata date; 
  set i = 0;
  select dayofyear(concat(anno,'-12-31')) into ultimo;
  while i < ultimo do
    select concat(anno,'-01-01') + interval i day into miadata;
    insert into dates (pday) values (miadata);
    set i = i + 1;
  end while;
end $$
delimiter ;

call calendario(2011);

If you have a table RENTAL too (the real version would need a lot of other details in it):

CREATE TABLE Rental
(
    start   DATE NOT NULL,
    end     DATE NOT NULL
);

and you populate it with:

INSERT INTO rental VALUES('2011-08-20', '2011-09-05');
INSERT INTO rental VALUES('2011-08-20', '2011-08-25');

then this query produces a plausible result:

SELECT  r.start AS r_start, r.end AS r_end,
        s.start AS s_start, s.end AS s_end,
        GREATEST(r.start, s.start) AS p_start,
        LEAST(r.end, s.end)        AS p_end,
        DATEDIFF(LEAST(r.end, s.end), GREATEST(r.start, s.start)) + 1 AS days,
        s.id, s.slug
  FROM rental AS r
  JOIN season_dates AS s ON r.start <= s.end AND r.end >= s.start;

It yields:

r_start     r_end       s_start     s_end       p_start     p_end       days id slug
2011-08-20  2011-09-05  2011-07-01  2011-08-31  2011-08-20  2011-08-31  12   3  high
2011-08-20  2011-09-05  2011-09-01  2011-10-31  2011-09-01  2011-09-05   5   4  mid 
2011-08-20  2011-08-25  2011-07-01  2011-08-31  2011-08-20  2011-08-25   6   3  high

Note that I'm counting 12 days instead of 11; that's the +1 in the days expression. It gets tricky; you have to decide whether if the car is returned on the same day as it is rented, is that one day's rental? What if it is returned the next day? Maybe the time matters? But that gets into detailed business rules rather than general principles. Maybe the duration is the larger of the raw DATEDIFF() and 1? Also note that there is only the rental start and end dates in this schema to identify the rental; a real schema would have some sort of Rental Agreement Number in the rental table.

(Confession: simulated using IBM Informix 11.70.FC2 on MacOS X 10.7.1, but MySQL is documented as supporting LEAST, GREATEST, and DATEDIFF and I simulated those in Informix. The most noticeable difference might be that Informix has a DATE type without any time component, so there are no times needed or displayed.)


But [...] seasons period always same every year. So I thought to compare only days and months. 2011 isn't important. Next years just 2011 will be used. This time problem occurs. For example low season includes November, December and then go to January, February, March, April. If a user selects a date range 01.05.2011 to ...2011 There is no problem. I just compare month and day with DATE_FORMAT(end, '%m/%d'). But if he chooses a range from December to next year January, how am I gonna calculate days?

Notice that 5 entries per year in the Season_Dates table is not going to make an 8" floppy disk break sweat over storage capacity for a good few years, let alone a 500 GiB monster disk. So, by far the simplest thing is to define the entries for 2012 in 5 new rows in the Season_Dates table. That also allows you to handle the fact that in December, the powers-that-be decide the rules will be different (20th December to 4th January will be 'mid', not 'low' season, for example).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM