Я не уверен, что это лучший способ, но вот один из способов сделать это в 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 (сегодня). Затем мы делаем LEFT OUTER JOIN с нашей таблицей posts
. Это дает нам одну строку для каждого дня в диапазоне, с количеством сообщений в этот день в столбце 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
Теперь мы знаем, что есть почта на каждый день с 14-17 декабря, так что если сегодня 17 декабря мы знаем, что ток «полоса» 4 дня. Мы могли бы сделать еще несколько SQL, чтобы получить, например. самая длинная полоса, как described in this article, но поскольку нас интересует только длина «текущей» полосы, она просто немного изменится. Все, что нам нужно сделать, это изменить наш запрос, чтобы получить только первую дату, на которую num_posts_on_date
является 0
(SQL Fiddle):
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
Но так как мы на самом деле хотим, чтобы количество дней после последнего дня без каких-либо сообщений, мы можем сделать это в 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, и заменить наши жестко закодированные даты с запросом, который получает дату первого сообщения пользователя (подпункт -выберите внутри функции 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
выше не тестировался, так что, скорее всего, займет некоторое возился, чтобы заставить его работать , но я надеюсь, что это указывает на то, что вы в правильном направлении.
Лично я бы вычислил длину полосы каждый раз, когда запрашивалась страница профиля. – Adrian
Какая база данных? MySQL? Postgres? –
Я использую Postgres – Andrew