在一个简单的Ruby on Rails应用程序中,我正在尝试计算用户发布的连续天数。例如,如果我在过去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)