计算在Rails中连续发布的天数

And*_*rew 4 ruby ruby-on-rails

在一个简单的Ruby on Rails应用程序中,我试图计算用户发布的连续天数。因此,举例来说,如果我过去4天中的每一天都发布了邮件,我希望在个人资料中显示“您当前的发布时间是4天,请继续努力!” 或类似的东西。

我应该跟踪其中一个模型中的“条纹”,还是应该在其他位置进行计算?不知道我应该在哪里做,或如何正确做,所以任何建议都会很棒。

很高兴包含您认为有用的任何代码,请告诉我。

Jor*_*ing 5

我不确定这是否是最好的方法,但是这是在SQL中执行此操作的一种方法。首先,看看下面的查询。

SELECT
  series_date,
  COUNT(posts.id) AS num_posts_on_date
FROM generate_series(
       '2014-12-01'::timestamp,
       '2014-12-17'::timestamp,
       '1 day'
     ) AS series_date
LEFT OUTER JOIN posts ON posts.created_at::date = series_date
GROUP BY series_date
ORDER BY series_date DESC;
Run Code Online (Sandbox Code Playgroud)

我们使用generate_series生成的日期范围从2014年12月1日开始,到2014年12月17日结束(今天)。然后我们在posts桌子上做一个左外连接。这样一来,我们每天就可以在该范围内获得一行,而该num_posts_on_date列中该天的帖子数。结果看起来像这样(SQL Fiddle在这里):

 series_date                     | num_posts_on_date
---------------------------------+-------------------
 December, 17 2014 00:00:00+0000 |                 1
 December, 16 2014 00:00:00+0000 |                 1
 December, 15 2014 00:00:00+0000 |                 2
 December, 14 2014 00:00:00+0000 |                 1
 December, 13 2014 00:00:00+0000 |                 0
 December, 12 2014 00:00:00+0000 |                 0
 ...                             |               ...
 December, 01 2014 00:00:00+0000 |                 0
Run Code Online (Sandbox Code Playgroud)

现在我们知道从12月14日至17日每天都有帖子,所以如果今天是12月17日,我们知道当前的“连胜”是4天。如本文所述,我们可以做一些更多的SQL以获取最长的连胜记录,但是由于我们只对“当前”连胜记录的长度感兴趣,因此只需要进行很小的更改即可。我们所要做的是改变我们的查询只得到了第一次约会的这num_posts_on_date0SQL小提琴):

SELECT series_date
FROM generate_series(
       '2014-12-01'::timestamp,
       '2014-12-17'::timestamp,
       '1 day'
     ) AS series_date
LEFT OUTER JOIN posts ON posts.created_at::date = series_date
GROUP BY series_date
HAVING COUNT(posts.id) = 0
ORDER BY series_date DESC
LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

结果:

 series_date
---------------------------------
 December, 13 2014 00:00:00+0000
Run Code Online (Sandbox Code Playgroud)

但是由于我们实际上想要的是自上一天以来没有帖子的天数,因此我们也可以在SQL中执行此操作(SQL Fiddle):

SELECT ('2014-12-17'::date - series_date::date) AS days
FROM generate_series(
       '2014-12-01'::timestamp,
       '2014-12-17'::timestamp,
       '1 day'
     ) AS series_date
LEFT OUTER JOIN posts ON posts.created_at::date = series_date
GROUP BY series_date
HAVING COUNT(posts.id) = 0
ORDER BY series_date DESC
LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

结果:

 days
------
    4
Run Code Online (Sandbox Code Playgroud)

你去!

现在,如何将其应用于我们的Rails代码?像这样:

qry = <<-SQL
  SELECT (CURRENT_DATE - series_date::date) AS days
  FROM generate_series(
         ( SELECT created_at::date FROM posts
           WHERE posts.user_id = :user_id
           ORDER BY created_at
           ASC LIMIT 1
         ),
         CURRENT_DATE,
         '1 day'
       ) AS series_date
  LEFT OUTER JOIN posts ON posts.user_id = :user_id AND
                           posts.created_at::date = series_date
  GROUP BY series_date
  HAVING COUNT(posts.id) = 0
  ORDER BY series_date DESC
  LIMIT 1
SQL

Post.find_by_sql([ qry, { user_id: some_user.id } ]).first.days # => 4
Run Code Online (Sandbox Code Playgroud)

如您所见,我们添加了一个条件来限制user_id的结果,并用一个查询替换了我们的硬编码日期,该查询获取了用户generate_series在该范围的开头的第一条帖子(功能内的子选择)的日期和CURRENT_DATE在范围的末尾。

最后一行有点有趣,因为find_by_sql它将返回一个Post实例数组,因此您必须days在数组中的第一个实例上调用以获取值。或者,您可以执行以下操作:

sql = Post.send(:sanitize_sql, [ qry, { user_id: some_user.id } ])
result_value = Post.connection.select_value(sql)
streak_days = Integer(result_value) rescue nil # => 4
Run Code Online (Sandbox Code Playgroud)

在ActiveRecord中,可以使其变得更干净:

class Post < ActiveRecord::Base
  USER_STREAK_DAYS_SQL = <<-SQL
    SELECT (CURRENT_DATE - series_date::date) AS days
    FROM generate_series(
          ( SELECT created_at::date FROM posts
            WHERE posts.user_id = :user_id
            ORDER BY created_at ASC
            LIMIT 1
          ),
          CURRENT_DATE,
          '1 day'
        ) AS series_date
    LEFT OUTER JOIN posts ON posts.user_id = :user_id AND
                             posts.created_at::date = series_date
    GROUP BY series_date
    HAVING COUNT(posts.id) = 0
    ORDER BY series_date DESC
    LIMIT 1
  SQL

  def self.user_streak_days(user_id)
    sql = sanitize_sql [ USER_STREAK_DAYS_SQL, { user_id: user_id } ]
    result_value = connection.select_value(sql)
    Integer(result_value) rescue nil
  end
end

class User < ActiveRecord::Base
  def post_streak_days
    Post.user_streak_days(self)
  end
end

# And then...
u = User.find(123)
u.post_streak_days # => 4
Run Code Online (Sandbox Code Playgroud)

上面的代码未经测试,因此可能需要花些时间才能使其生效,但我希望至少可以为您指明正确的方向。