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

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

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

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

我不确定它是否是最好的方法,但这是在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列中有post数。 结果看起来像这样( 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日有一个post,所以如果今天的12月17日我们知道当前的“连胜”是4天。 如本文所述,我们可以做更多的SQL来获得最长的条纹,但由于我们只对“当前”条纹的长度感兴趣,所以它只需要稍作改动。 我们所要做的就是更改查询以仅获取num_posts_on_date0的第一个日期( SQL小提琴 ):

 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; 

结果如下:

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

但是因为我们实际上想要自上一天以来没有post的天数,我们也可以在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; 

结果:

  days ------ 4 

你去!

现在,如何将它应用于我们的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 

如您所见,我们添加了一个条件来限制user_id的结果,并用一个查询替换我们的硬编码日期,该查询获取用户的第一个post的日期( 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 

在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 

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

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

假设post属于用户。

发布模型

 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 

我相信安德鲁的回答会奏效。 不可否认,我可能会过度思考这个解决方案,但是如果你想要一个不需要维护条纹列的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 = ? 

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

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

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

这里可以找到另一个不错的解决方案 使用此代码,即使您当时的用户今天没有任何post,您也可以查看昨天的连续日期。 它将激励用户继续他们的连胜。

 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)