查找可用的公寓,包括日期范围,持续时间和不可用日期列表

时间:2014-08-16 17:30:38

标签: sql ruby postgresql bit-manipulation

我有一个谜题,我希望有人可以帮我解决

我有一张公寓房源表。每个商家信息都有一个或多个“不可用”日期分别存储在另一个表格中,我们称之为“off_days”。例如从9月1日到4月4日不可用的列表将在'off_days'表中有4个条目,每天一个。

我正在寻找最有效的搜索方式(最好是在数据库级别),以便在两个日历日之间至少有N个连续可用天数('可用'是任何一天不在'off_days'表中特别列表)。例如“在9月份至少连续5天显示所有列表”

我一直在思考如何在现实世界中解决这个问题(通过查看标有X的日历并扫描空闲块)并开始考虑使用二进制来表示可用/不可用的天数。即对于给定的一周,0111001(= 57)会告诉我该周连续可用的天数最多为三天。

这个question似乎是一个良好的开端,一旦我有给定日期范围的二进制数,但现在我仍然坚持如何在给定日期范围内动态计算该数字,再次,在数据库上水平....任何想法?或者对这种方法或其他方法的看法?

3 个答案:

答案 0 :(得分:2)

在休息日,公寓可供出租。这意味着您想知道每个序列的差距有多大,lag()函数可以为您提供以下信息:

select od.*,
       lag(unavailable) over (partition by apartmentid order by unavailable) as prev_una
from offdays od;

实际天数是不可用和上一个减1之间的差异。现在,假设两个日历日为v_StartDatev_EndDate。现在你基本上可以得到你想要的东西:

select od.*,
       ((case when unavailable is NULL or unavailable > v_EndDate
              then v_EndDate + 1 else unavailable
         end) -
        (case when prev_una is null or prev_una < v_StartDate
              then v_StartDate - 1 else prev_una
         end) - 1
       ) as days_available
from (select od.*, lag(unavailable) over (partition by apartmentid order by unavailable) as prev_una
      from offdays od
     ) od
order by days_available desc;

case逻辑基本上是在句号之前和之后放置停止日期。

这并不完全,因为它有边界问题:公寓不在offdays时出现问题,而在不可用时段超出范围时出现问题。我们使用union all和一些过滤来解决此问题:

select od.*,
       ((case when unavailable is NULL or unavailable > v_EndDate
              then v_EndDate + 1 else unavailable
         end) -
        (case when prev_una is null or prev_una < v_StartDate
              then v_StartDate - 1 else prev_una
         end) - 1
       ) as days_available
from (select od.apartmentId, unavailable,
             lag(unavailable) over (partition by apartmentid order by unavailable) as prev_una
      from offdays od
      where od.unavailable between v_StartDate and v_EndDate
      union all
      select apartmentid, NULL, NULL
      from apartments a
      where not exists (select 1
                        from offdays od
                        where od.apartmentid = a.apartmentid and
                              od.unavailable between v_StartDate and v_EndDate
                       )
     ) od
order by days_available desc;

答案 1 :(得分:0)

即使我对Rails一无所知,我还是建议采用一种方法。如果我的建议毫无意义,请告诉我,我将删除我的答案并随意删除。

假设您仅询问数据库以创建一个数组,该数组指示公寓在连续日期范围内的每个日期是否可用。例如,假设它看起来像这样:

A = true
U = nil
avail = [A,A,A,U,U,A,A,U,U,A,A,A,A,A,U,A]

对于给定数量的连续日期n,可以通过以下方法给出开始运行至少n天的日期偏移。

<强>代码

def runs(avail, n)
  avail.each_with_index.each_cons(n).map do |run|
    av, off = run.transpose
    (av == av.compact) ? off.first : nil
  end.compact
end

<强>实施例

runs(avail,1) #=> [0, 1, 2, 5, 6, 9, 10, 11, 12, 13, 15]
runs(avail,2) #=> [0, 1, 5, 9, 10, 11, 12]
runs(avail,3) #=> [0, 9, 10, 11]
runs(avail,4) #=> [9, 10]
runs(avail,5) #=> [9]
runs(avail,6) #=> []

<强>解释

考虑上面的n=3案例。

n = 3
enum0 = avail.each_with_index
  #=> #<Enumerator: [true, true, true, nil, nil, true, true, nil, nil,
  #                  true, true, true, true, true, nil, true]:each_with_index>
enum0.to_a
  #=> [[true, 0], [true, 1], [true, 2], [nil, 3], [nil, 4], [true, 5],
  #    [true, 6], [nil, 7], [nil, 8], [true, 9], [true, 10], [true, 11],
  #    [true, 12], [true, 13], [nil, 14], [true, 15]]
enum1 = enum0.each_cons(n)
  #=> #<Enumerator: #<Enumerator: [true, true, true, nil,...
  #                           ..., true]:each_with_index>:each_cons(40)>
enum1.to_a
  #=> [[[true, 0], [true, 1], [true, 2]],
  #    [[true, 1], [true, 2], [nil, 3]],
  #    ...
  #    [[true, 13], [nil, 14], [true, 15]]]
enum2 = enum1.map
  #=> #<Enumerator: #<Enumerator: #<Enumerator: [true, true, true, nil,...
  #                           ...true]:each_with_index>:each_cons(3)>:map>
enum2.to_a
  #=> [[[true, 0], [true, 1], [true, 2]],
  #    [[true, 1], [true, 2], [nil, 3]],
  #    ...
  #    [[true, 13], [nil, 14], [true, 15]]]    
a = enum2.each do |run|
  av, off = run.transpose
  (av == av.compact) ? off.first : nil
end
  #=> [0, nil, nil, nil, nil, nil, nil, nil, nil, 9, 10, 11, nil, nil]
a.compact
  #=> [0, 9, 10, 11]

考虑上面的数组a的计算。 enum2传递给块的each的第一个元素,并分配给块变量run

run => [[true, 0], [true, 1], [true, 2]]

然后

av, off = run.transpose #=> [[true, true, true], [0, 1, 2]]
av                      #=> [true, true, true]
off                     #=> [0, 1, 2]
([true, true, true] == [true, true, true].compact) ? 0 : nil
  #=> ([true, true, true] == [true, true, true]) ? 0 : nil
  #=> 0

因此enum2的第一个值会映射到0,这意味着从偏移量3开始至少有0天的运行。

接下来,[[true, 1], [true, 2], [nil, 3]]被传递到块中并分配给变量run。然后:

av, off = run.transpose #=> [[true, true, nil], [1, 2, 3]]
av                      #=> [true, true, nil]
off                     #=> [1, 2, 3]
([true, true, nil] == [true, true, nil].compact) ? 1 : nil
  # ([true, true, nil] == [true, true]) ? 1 : nil
  #=> nil

因此enum2的第二个值会映射到nil,这意味着从偏移量3开始,至少1天没有。等等...

备注

  • avail中指示公寓的值在给定的日期(上面的常数A)可用,可以是任何“真实”值(即false以外的任何值或nil);但是,不可用日期(由上面U表示)必须由nil表示,因为我使用Array#compact将其从数组中删除。
  • enum1enum2视为“复合”枚举数可能会有所帮助。
  • enum0.to_aenum1.to_aenum2.to_a用于显示每个枚举器将传递到其块中的内容(如果有的话)。 (enum2当然有一个区块。)

答案 2 :(得分:0)

候选公寓

使用generate_series()和两个嵌套的EXISTS表达式,您可以将这个英语句子直接翻译成SQL:

Find apartments  
  where at least one day exists in a given time range (September)
    where no "off_day" exists in a 5-day range starting that day.
SELECT *
FROM   apt a
WHERE  EXISTS (
   SELECT 1
   FROM   generate_series('2014-09-01'::date
                        , '2014-10-01'::date - 5
                        , interval '1 day') r(day)
   WHERE  NOT EXISTS (
      SELECT 1
      FROM   offday o
      WHERE  o.apt_id = a.apt_id
      AND    o.day BETWEEN r.day::date AND r.day::date + 4
      )
   )
ORDER BY a.apt_id;  -- optional

空闲时段

您可以应用类似的查询来获取实际免费广告位列表(开始日期):

SELECT *
FROM   apt a
-- FROM   (SELECT * FROM apt a WHERE apt_id = 1) a  -- for just agiven apt
CROSS  JOIN generate_series('2014-09-01'::date
                          , '2014-10-01'::date - 5
                          , interval '1 day') r(day)
WHERE  NOT EXISTS (
   SELECT 1
   FROM   offday o
   WHERE  o.apt_id = a.apt_id
   AND    o.day BETWEEN r.day::date AND r.day::date + 4
   )
ORDER BY a.apt_id, r.day;

仅适用于给定的。:

中的时段
SELECT *
FROM  (SELECT * FROM apt a WHERE apt_id = 1) a
...

SQL Fiddle.

数据库设计

如果 “off-days”通常包含连续多天,则基于date ranges的替代表格布局而不是单日是一个(更有效)的替代方案。

Range operators可以在范围列上使用GiST索引。我的第一个答案草案建立在ad-hoc范围(see answer history)上,但在使用“每天一行”设计时,更新的解决方案更简单,更快捷。具有适应性查询的替代布局:

SQL Fiddle with range types.

相关答案与基本信息的类似实施: