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

时间:2014-12-17 20:01:29

标签: ruby-on-rails ruby

在一个简单的Ruby on Rails应用程序中,我正在尝试计算用户发布的连续天数。例如,如果我在过去4天中发布了每一天,我想在我的个人资料中说“你目前的帖子连续4天,继续保持!”或类似的东西。

我应该跟踪一个模型中的“条纹”,还是应该在其他地方计算它们?不确定我应该在哪里做,或者如何正确地做到这一点,所以任何建议都会很精彩。

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

4 个答案:

答案 0 :(得分:3)

我不确定这是不是最好的方法,但这是在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;

我们使用generate_series生成从2014-12-01开始到2014-12-17(今天)结束的日期范围。然后我们使用posts表进行LEFT OUTER JOIN。这使我们在该范围内的每一天都有一行,num_posts_on_date列中当天的帖子数量。结果如下(SQL Fiddle here):

 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

现在我们知道从12月14日到17日每天都有一个帖子,所以如果今天的12月17日我们知道当前的“连胜”是4天。我们可以做更多的SQL来获得最长的连胜,described in this article,但由于我们只对“当前”连胜的长度感兴趣,所以只需稍作改动。我们所要做的就是更改查询以仅获取num_posts_on_date 0 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; SQL Fiddle)的第一个日期:

 series_date
---------------------------------
 December, 13 2014 00:00:00+0000

结果:

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;

但是因为我们实际上想要自上一天以来没有帖子的天数,我们也可以在SQL中做到这一点(SQL Fiddle):

 days
------
    4

结果:

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

你去吧!

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

generate_series

正如您所看到的,我们添加了一个条件来限制user_id的结果,并用一个获取用户第一个帖子日期的查询替换我们的硬编码日期(CURRENT_DATE函数内的子选择)范围的开头和范围结束的find_by_sql

最后一行有点搞笑,因为days将返回一个Post实例数组,因此您必须在数组中的第一个上调用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 来获取值。或者,您可以这样做:

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

在ActiveRecord中,它可以变得更清洁:

{{1}}

以上是未经测试的,因此可能需要一些小动作才能使其正常工作,但我希望它至少能指向正确的方向。

答案 1 :(得分:2)

我会在用户模型中创建两列。 “streak_start”和“streak_end”是时间戳。

假设帖子属于用户。

发布模型

after_create :update_streak  
def update_streak
    if self.user.streak_end > 24.hours.ago
        self.user.touch(:streak_end)
    else
        self.user.touch(:streak_start)
        self.user.touch(:streak_end)
    end
end

就我个人而言,我会这样写:

def update_streak
    self.user.touch(:streak_start) unless self.user.streak_end > 24.hours.ago
    self.user.touch(:streak_end)
end

然后确定用户的连胜。

用户模型

def streak
    # put this in whatever denominator you want
    self.streak_end > 24.hours.ago ? (self.streak_end - self.streak_start).to_i : 0
end

答案 2 :(得分:0)

我相信安德鲁的回答会奏效。不可否认,我可能会过度思考这个解决方案,但是如果你想要一个不需要维护条纹列的SQL专注解决方案,你可以尝试这样的事情:

SELECT 
    *, COUNT(diff_from_now)
FROM
    (SELECT 
        p1.id,
        p1.user_id,
        p1.created_at,
        (DATEDIFF(p1.created_at, p2.created_at)) AS diff,
        DATEDIFF(NOW(), p1.created_at) AS diff_from_now
    FROM
        posts p1
    LEFT JOIN (SELECT 
        *
    FROM
        posts
    ORDER BY created_at DESC) p2 ON DATE(p2.created_at) = DATE(p1.created_at) + INTERVAL 1 DAY
    WHERE
        (DATEDIFF(p1.created_at, p2.created_at)) IS NOT NULL
    ORDER BY (DATEDIFF(p1.created_at, p2.created_at)) DESC , created_at DESC) inner_query
GROUP BY id, sender_id, created_at, diff, diff_from_now, diff_from_now
HAVING COUNT(diff_from_now) = 1
where user_id = ?

简而言之,最里面的查询计算该帖子与下一个连续帖子之间的日期差异,同时还计算该帖子与当前日期的差异。外部查询然后过滤日期差异序列未增加一天的任何内容。

请注意:此解决方案仅在MySQL中进行了测试,虽然我看到您将Postgres指示为您的数据库,但我现在还没有时间将这些功能正确地更改为Postgres使用。我很快就会对这个问题进行适当的补充,但我认为尽快看到这个可能是有益的。更新此帖后,此说明也将被删除。

您应该能够将其作为原始SQL执行。也可以将其转换为Active Record,我在更新这篇文章时可能会这样做。

答案 3 :(得分:0)

可以找到另一个不错的解决方案here。使用此代码,即使您当时的用户今天没有任何帖子,您也可以看到昨天的连续几天。这将激励用户继续前进。

def get_last_user_posts_steak
    qry = <<-SQL
        WITH RECURSIVE CTE(created_at)
        AS
        (
           SELECT * FROM 
           (
              SELECT created_at FROM posts WHERE posts.user_id = :user_id AND ( created_at::Date = current_date 
              OR created_at::Date = current_date - INTERVAL '1 day' )
              ORDER BY created_at DESC
              LIMIT 1
           ) tab
           UNION ALL

           SELECT a.created_at FROM posts a
           INNER JOIN CTE c
           ON a.created_at::Date  = c.created_at::Date  - INTERVAL '1 day' AND a.user_id = :user_id
           GROUP BY a.created_at
        ) 
        SELECT COUNT(*) FROM CTE;
    SQL

    sql = sanitize_sql [ qry, { user_id: user.id } ]
    result_value = connection.select_value(sql)
    return Integer(result_value) rescue 0
end

Result = Post.get_last_user_posts_steak(current_user)