Skip to content

Commit

Permalink
Move calculation into Fingerprint model
Browse files Browse the repository at this point in the history
** Why are these changes being introduced:

It makes sense that the Fingerprint model is where the logic to
calculate a fingerprint value is maintained, and not in the Term model.

** Relevant ticket(s):

code review

** How does this address that need:

This moves the fingerprint logic from the Term to Fingerprint model.
Because of this, the Term model changes to use its new location (from
inside the lifecycle hook where the Fingerprint record gets created).

We use an instance method for this, because at the time of use there is
not yet a Fingerprint record that could call the method internally.

We also copy-paste the tests for this method from the SuggestedResource
implementation (which should be removed in a future PR).

** Document any side effects to this change:

I can think of two possible side effects:
1. the SuggestedResource implementation of the fingerprint is now very
much duplicative, and should be removed (coming in a future ticket)

2. It is a bit awkward to rely on an instance method for calculating
the fingerprint value. In an ideal case, register_fingerprint would
use something like:

self.fingerprint = Fingerprint.find_or_create_by(phrase)

... and then the Fingerprint model would:
a. receive the phrase argument
b. calculate the fingerprint for this phrase
c. look up to see if such a record exists already, and return it
d. create the record if none exists, and return _that_

A custom initialize method feels like it would work for this, but I
think that's considered an antipattern in Rails?

For now, I think this works.
  • Loading branch information
matt-bernhardt committed Dec 11, 2024
1 parent c05c4ed commit 0930853
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 21 deletions.
17 changes: 17 additions & 0 deletions app/models/fingerprint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,21 @@ class Fingerprint < ApplicationRecord
validates :fingerprint, uniqueness: true

alias_attribute :fingerprint_value, :fingerprint

# This is similar to the SuggestedResource fingerprint method, with the exception that it also replaces &quot; with "
# during its operation. This switch may also need to be added to the SuggestedResource method, at which point they can
# be abstracted to a helper method.
def self.calculate(phrase)
modified = phrase
modified = modified.strip
modified = modified.downcase
modified = modified.gsub('&quot;', '"') # This line does not exist in SuggestedResource implementation.
modified = modified.gsub(/\p{P}|\p{S}/, '')
modified = modified.to_ascii
modified = modified.gsub(/\p{P}|\p{S}/, '')
tokens = modified.split
tokens = tokens.uniq
tokens = tokens.sort
tokens.join(' ')
end
end
28 changes: 7 additions & 21 deletions app/models/term.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Term < ApplicationRecord
has_many :confirmations, dependent: :destroy
belongs_to :fingerprint, optional: true

before_save :store_fingerprint
before_save :register_fingerprint
after_destroy :check_fingerprint_count

scope :user_confirmed, -> { where.associated(:confirmations).distinct }
Expand Down Expand Up @@ -87,27 +87,13 @@ def calculate_categorizations

private

# The store_fingerprint method gets called before a Term record is saved, ensuring that Terms should always have a
# register_fingerprint method gets called before a Term record is saved, ensuring that Terms should always have a
# related Fingerprint method.
def store_fingerprint
self.fingerprint = Fingerprint.find_or_create_by({ fingerprint: calculate_fingerprint })
end

# This is similar to the SuggestedResource fingerprint method, with the exception that it also replaces &quot; with "
# during its operation. This switch may also need to be added to the SuggestedResource method, at which point they can
# be abstracted to a helper method.
def calculate_fingerprint
modified = phrase
modified = modified.strip
modified = modified.downcase
modified = modified.gsub('&quot;', '"') # This line does not exist in SuggestedResource implementation.
modified = modified.gsub(/\p{P}|\p{S}/, '')
modified = modified.to_ascii
modified = modified.gsub(/\p{P}|\p{S}/, '')
tokens = modified.split
tokens = tokens.uniq
tokens = tokens.sort
tokens.join(' ')
def register_fingerprint
new_record = {
fingerprint: Fingerprint.calculate(phrase)
}
self.fingerprint = Fingerprint.find_or_create_by(new_record)
end

# This is called during the after_destroy hook. If removing that term means that its fingerprint is now abandoned,
Expand Down
45 changes: 45 additions & 0 deletions test/models/fingerprint_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,49 @@ class FingerprintTest < ActiveSupport::TestCase
assert_nil target_term.fingerprint_value
assert_predicate target_term, :valid?
end

# These tests appear in order of operation within the calculate method.
test 'fingerprints strip excess spaces' do
example = ' i need space '

assert_equal 'i need space', Fingerprint.calculate(example)
end

test 'fingerprints are coerced to lower case' do
example = 'InterCapping FTW'

assert_equal 'ftw intercapping', Fingerprint.calculate(example)
end

test 'fingerprints strip out &quot;' do
example = '&quot;in quotes&quot;'

assert_equal 'in quotes', Fingerprint.calculate(example)
end

test 'fingerprints remove punctuation and symbols' do
example = 'symbols™ + punctuation: * bullets! - "quoted phrase" (perfect) ¥€$'

assert_equal 'bullets perfect phrase punctuation quoted symbols', Fingerprint.calculate(example)
end

test 'fingerprints coerce characters to ASCII' do
example = 'а а̀ а̂ а̄ ӓ б в г ґ д ђ ѓ е ѐ е̄ е̂ ё є ж з з́ ѕ и і ї ꙇ ѝ и̂ ӣ й ј к л љ м н њ о о̀ о̂ ō ӧ п р с с́ ' \
'т ћ ќ у у̀ у̂ ӯ ў ӱ ф х ц ч џ ш щ ꙏ ъ ъ̀ ы ь ѣ э ю ю̀ я'

assert_equal 'a b ch d dj dz dzh e f g gh gj i ia ie io iu j k kh kj l lj m n nj o p r s ' \
'sh shch t ts tsh u v y yi z zh', Fingerprint.calculate(example)
end

test 'fingerprints remove repeated words' do
example = 'double double'

assert_equal 'double', Fingerprint.calculate(example)
end

test 'fingerprints list words in alphabetical order' do
example = 'delta beta gamma alpha'

assert_equal 'alpha beta delta gamma', Fingerprint.calculate(example)
end
end

0 comments on commit 0930853

Please sign in to comment.