2014-12-17 5 views
3

В простом приложении Ruby on Rails я пытаюсь рассчитать количество последовательных дней, отправленных пользователем. Так, например, если бы я опубликовал каждый из последних 4 дней, я хотел бы иметь в своем профиле «Ваша текущая полоса проводок - это 4 дня, продолжайте!» или что-то типа того.Рассчитайте отправку дней подряд в Rails

Должен ли я отслеживать «полоски» в одной из моих моделей, или я должен рассчитать их в другом месте? Не знаю, где я должен это делать, или как правильно это сделать, поэтому любые предложения были бы замечательными.

Я рад включить любой код, который вы найдете полезным, просто дайте мне знать.

+1

Лично я бы вычислил длину полосы каждый раз, когда запрашивалась страница профиля. – Adrian

+0

Какая база данных? MySQL? Postgres? –

+0

Я использую Postgres – Andrew

ответ

2

Я не уверен, что это лучший способ, но вот один из способов сделать это в 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 

выше не тестировался, так что, скорее всего, займет некоторое возился, чтобы заставить его работать , но я надеюсь, что это указывает на то, что вы в правильном направлении.

+0

Спасибо за ваш невероятно подробный и проницательный ответ, похоже, он работает в моем ограниченном тестировании до сих пор! – Andrew

1

Я бы создал две колонки в пользовательской модели. «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 
0

Я считаю, что ответ Эндрю будет работать. Следует признать, что я могу быть overthinking это решение, но если вы хотели бы получить 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. Также можно преобразовать это в активную запись, что, скорее всего, сделаю, когда я обновлю это сообщение.

Смежные вопросы