Skip to content

Commit

Permalink
Merge pull request #23 from justCxx/exercise-17
Browse files Browse the repository at this point in the history
Complete Exercise 17
  • Loading branch information
v-kolesnikov committed Aug 26, 2015
2 parents 920f1e7 + 7d636dd commit 669c8a3
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 107 deletions.
4 changes: 2 additions & 2 deletions app/controllers/reviews_controller.rb
Expand Up @@ -5,7 +5,7 @@ def new

def create
@card = current_user.cards.find(review_params[:card_id])
review = @card.review(review_params[:answer])
review = @card.review(review_params[:answer], review_params[:quality].to_i)

if review[:success]
flash[:success] = t("review_success",
Expand All @@ -24,6 +24,6 @@ def create
protected

def review_params
params.require(:review).permit(:card_id, :answer)
params.require(:review).permit(:card_id, :answer, :quality)
end
end
58 changes: 14 additions & 44 deletions app/models/card.rb
@@ -1,6 +1,4 @@
class Card < ActiveRecord::Base
MAX_CORRECT_ANSWERS = 5
MAX_INCORRECT_ANSWERS = 3
MAX_LEVENSHTEIN_DISTANCE = 1

belongs_to :deck
Expand All @@ -9,7 +7,7 @@ class Card < ActiveRecord::Base
validate :words_different
validates_associated :deck

before_validation :set_review_date, if: :new_record?
after_initialize :set_default_date, if: :new_record?

has_attached_file :image, styles: { medium: "360x360", thumb: "100x100" },
default_url: "cards/missing/:style/missing.png"
Expand All @@ -22,66 +20,38 @@ def self.for_review
cards.offset(rand(cards.count))
end

def review(translated)
def review(translated, quality)
typos = words_distanse(translated, original_text)

if typos <= MAX_LEVENSHTEIN_DISTANCE
handle_correct_answer
else
handle_incorrect_answer
end

{ success: typos <= MAX_LEVENSHTEIN_DISTANCE, typos: typos }
success = typos <= MAX_LEVENSHTEIN_DISTANCE
update_review_date(success ? quality : 0)
{ success: success, typos: typos }
end

protected

def handle_correct_answer
increment(:correct_answers) if correct_answers < MAX_CORRECT_ANSWERS
update_attributes(incorrect_answers: 0)
update_review_date
def update_review_date(quality)
repetition = SuperMemo2.repetition(e_factor, interval, quality, repetitions)
repetition[:review_date] = DateTime.now + repetition[:interval]
update_attributes(repetition)
end

def handle_incorrect_answer
decrement(:correct_answers) if correct_answers > 0
increment(:incorrect_answers) if incorrect_answers < MAX_INCORRECT_ANSWERS
save
def words_distanse(word1, word2)
DamerauLevenshtein.distance(normalize(word1), normalize(word2))
end

private

def normalize(str)
str.squish.mb_chars.downcase.to_s
end

def set_review_date
def set_default_date
self.review_date = DateTime.now
end

def update_review_date
case correct_answers
when 0
offset = 0
when 1
offset = 12.hour
when 2
offset = 3.day
when 3
offset = 1.week
when 4
offset = 2.week
else
offset = 1.month
end

update_attributes(review_date: review_date + offset)
end

def words_different
if normalize(original_text) == (translated_text)
errors.add(:original_text, "can't equal translated text")
end
end

def words_distanse(word1, word2)
DamerauLevenshtein.distance(normalize(word1), normalize(word2))
end
end
75 changes: 75 additions & 0 deletions app/services/super_memo2.rb
@@ -0,0 +1,75 @@
# This class implement a _SuperMemo2_ algorithm.
# See more: http://www.supermemo.com/english/ol/sm2.htm
#
# Algorithm _SM-2_ used in the computer-based variant of the SuperMemo method
# and involving the calculation of easiness factors for particular items:
#
# 1. Split the knowledge into smallest possible items.
# 2. With all items associate an E-Factor equal to 2.5.
# 3. Repeat items using the following intervals:
# I(1) = 1
# I(2) = 6
# for n > 2: I(n) = I(n-1) * EF
#
# where:
# *I*(*n*) - inter-repetition interval after the n-th repetition (in days),
# *EF* - E-Factor of a given item
# If interval is a fraction, round it up to the nearest integer.
# 4. After each repetition assess the quality of repetition response in 0-5
# grade scale:
# 5 - perfect response
# 4 - correct response after a hesitation
# 3 - correct response recalled with serious difficulty
# 2 - incorrect response; where the correct one seemed easy to recall
# 1 - incorrect response; the correct one remembered
# 0 - complete blackout.
# 5. After each repetition modify the E-Factor of the recently repeated item
# according to the formula:
# E-Factror = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
# where:
# *E-Factor* - new value of the E-Factor,
# *EF* - old value of the E-Factor,
# *q* - quality of the response in the 0-5 grade scale.
# If *EF* is less than 1.3 then let EF be 1.3.
# 6. If the quality response was lower than 3 then start repetitions for the
# item from the beginning without changing the E-Factor (i.e. use intervals
# I(1), I(2) etc. as if the item was memorized anew).
# 7. After each repetition session of a given day repeat again all items that
# scored below four in the quality assessment. Continue the repetitions until
# all of these items score at least fou
class SuperMemo2
MIN_E_FACTOR = 1.3
MIN_QUALITY = 3

# Return repetition result
def self.repetition(e_factor, interval, quality, repetitions)
repetitions = reduce_repetitions(repetitions, quality)
{
e_factor: e_factor(e_factor, quality),
interval: interval(e_factor, interval, repetitions).days,
quality: quality,
repetitions: repetitions + 1
}
end

# Return interval unto the next repetition (see p.3 of class description).
def self.interval(e_factor, interval, repetitions)
case repetitions
when 0 then 1
when 1 then 6
else
(interval * e_factor).ceil
end
end

# Return E-Factor according to the formula (see p.5 of class description).
def self.e_factor(e_factor, quality)
ef = e_factor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
[MIN_E_FACTOR, ef].max
end

# Return count of repetitions (see p.3 of class description).
def self.reduce_repetitions(repetitions, quality)
quality < MIN_QUALITY ? 0 : repetitions
end
end
8 changes: 3 additions & 5 deletions app/views/cards/index.html.erb
Expand Up @@ -6,8 +6,7 @@
<th><%= Card.human_attribute_name(:translated_text) %></th>
<th><%= Card.human_attribute_name(:image) %></th>
<th><%= Card.human_attribute_name(:review_date) %></th>
<th><%= Card.human_attribute_name(:correct_answers) %></th>
<th><%= Card.human_attribute_name(:incorrect_answers) %></th>
<th><%= Card.human_attribute_name(:repetitions) %></th>
<th><%= Deck.human_attribute_name(:title) %></th>
</tr>

Expand All @@ -16,9 +15,8 @@
<td><%= card.original_text %></td>
<td><%= card.translated_text %></td>
<td><%= image_tag card.image.url(:thumb) %></td>
<td><%= card.review_date.strftime("%d/%m/%Y %H:%M") %></td>
<td><%= card.correct_answers %></td>
<td><%= card.incorrect_answers %></td>
<td><%= card.review_date.localtime.strftime("%d/%m/%Y %H:%M") %></td>
<td><%= card.repetitions %></td>
<td><%= card.deck.title %></td>
<td><%= link_to t(:edit), edit_card_path(card) %></td>
<td><%= link_to t(:destroy), card_path(card), method: :delete %></td>
Expand Down
1 change: 1 addition & 0 deletions app/views/reviews/_form.html.erb
Expand Up @@ -5,5 +5,6 @@
<p><%= card.translated_text %></p>
</div>
<%= f.input :answer, label: (t :review_answer) %>
<%= f.input :quality, label: (t :review_quality), collection: 1..5 %>
<%= f.button :submit, (t :review_check) %>
<% end %>
2 changes: 1 addition & 1 deletion app/views/reviews/new.html.erb
@@ -1,5 +1,5 @@
<% if @card %>
<%= render 'form', card: @card %>
<%= render "form", card: @card %>
<% else %>
<p> Нет карточек для просмотра. </p>
<% end %>
1 change: 1 addition & 0 deletions config/locales/en.yml
Expand Up @@ -52,6 +52,7 @@ en:
review_answer: "Answer"
review_check: "Check"
review_wrong: "Wrong! Next review: %{next_review}"
review_quality: "Answer quality"
review_success: >
Right! Original: %{original},
translated: %{translated}.
Expand Down
4 changes: 2 additions & 2 deletions config/locales/ru.yml
Expand Up @@ -14,10 +14,9 @@ ru:
deck:
title: "Название"
card:
correct_answers: "Правильные ответы"
incorrect_answers: "Неправильные ответы"
image: "Изображение"
original_text: "Оригинал"
repetitions: "Повторения"
review_date: "Дата просмотра"
translated_text: "Перевод"
user:
Expand Down Expand Up @@ -56,6 +55,7 @@ ru:
review_answer: "Ответ"
review_check: "Проверить"
review_wrong: "Неверно! Следующий просмотр: %{next_review}"
review_quality: "Оценка ответа"
review_success: >
Правильно! Оригинал: %{original}, перевод: %{translated}.
Ваш ответ: %{user_answer}. Ошибок: %{typos}.
Expand Down
8 changes: 8 additions & 0 deletions db/migrate/20150824141658_add_super_memo_to_card.rb
@@ -0,0 +1,8 @@
class AddSuperMemoToCard < ActiveRecord::Migration
def change
add_column :cards, :e_factor, :float, default: 2.5
add_column :cards, :interval, :integer, default: 0
add_column :cards, :quality, :integer, default: 0
add_column :cards, :repetitions, :integer, default: 0
end
end
12 changes: 8 additions & 4 deletions db/schema.rb
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20150822114606) do
ActiveRecord::Schema.define(version: 20150824141658) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand All @@ -30,15 +30,19 @@
t.text "original_text"
t.text "translated_text"
t.datetime "review_date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "image_file_name"
t.string "image_content_type"
t.integer "image_file_size"
t.datetime "image_updated_at"
t.integer "deck_id", null: false
t.integer "deck_id", null: false
t.integer "correct_answers", default: 0
t.integer "incorrect_answers", default: 0
t.float "e_factor", default: 2.5
t.integer "interval", default: 0
t.integer "quality", default: 0
t.integer "repetitions", default: 0
end

add_index "cards", ["deck_id"], name: "index_cards_on_deck_id", using: :btree
Expand Down

0 comments on commit 669c8a3

Please sign in to comment.