likes 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f168d5f36ca20484399058ad67d22ca0962f2ffd
4
- data.tar.gz: db2cc8cf0d5b1dd4cefcf14fad30b2d0787c19ef
3
+ metadata.gz: 6148191736d05b2de8dce7c05ad3ec5781a70735
4
+ data.tar.gz: fdc47aec630718afa84d9c79f25e7a82bf71e8e5
5
5
  SHA512:
6
- metadata.gz: 24d2aed90f4038fbcb2c3563a0ceca7587eefd75adbd57f17a812e61a24eb1b04af435b2551c39cf1c0e1629e74ded23392ee25f39cf61a84b7bd8c1cbea6ffe
7
- data.tar.gz: 369523bb064ce679b8bc80c7cf431656da02257c2a46111f2028d59cb11ad0c204f81a23a922b41854291cb129a17bbbd0bc7a554d4bec0de927f6e07cab97b8
6
+ metadata.gz: 6f3b0458b662807a8ba0558be63846421a8d95a1bb0d64729419aeb08b58d0f6dfafa0fe4ff91fd3fae464833152d7ef8827fdea901106bf69dc360d0bee7033
7
+ data.tar.gz: 6e8d085e9022c1ead5877416f48b250bcd2036e62ce442d345156556bb2cfc461961efecf78cc0a89f181511983b071783f29087b20df26fd91a4f0726b37c8d
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in likes.gemspec
4
4
  gemspec
5
+
6
+ gem "rspec"
7
+ gem "yard"
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Likes
2
2
 
3
- Give it a list of people and their likings and it will tell it what else could these people like.
3
+ Give it a list of people and their likings and it will tell what else could these people like.
4
4
 
5
5
  This is a ruby gem.
6
6
 
@@ -22,9 +22,9 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- *Not implemented yet*
26
-
27
25
  ```ruby
26
+ require "likes"
27
+
28
28
  likeset = Likes::Set.new([
29
29
  Likes::Like.new(person: 1, item: 1),
30
30
  Likes::Like.new(person: 1, item: 5),
data/lib/likes/like.rb ADDED
@@ -0,0 +1,46 @@
1
+ module Likes
2
+ # Job: Understands relation between person and the item they like
3
+ class Like
4
+ # Creates new instance of Like
5
+ #
6
+ # @param [Hash] attributes The attributes to create Like instance with
7
+ # @option attributes [Person#==] :person The person under the question. Required
8
+ # @option attributes [Item#==] :item The item person like. Required
9
+ def initialize(attributes={})
10
+ @person = fetch_required_attribute(attributes, :person)
11
+ @item = fetch_required_attribute(attributes, :item)
12
+ end
13
+
14
+ # Tests other object for equality with itself. Objects of type
15
+ # different from Like are considered non-equal.
16
+ #
17
+ # @param [Any, Like] other Other object to test for equality
18
+ # @return [TrueClass, FalseClass] Returns true if objects are
19
+ # equal, false otherwise
20
+ def ==(other)
21
+ return false unless Like === other
22
+ self.person == other.person &&
23
+ self.item == other.item
24
+ end
25
+
26
+ # @private
27
+ def add_item_to_map(map)
28
+ (map[person] ||= []) << item
29
+ end
30
+
31
+ # @private
32
+ def add_person_to_map(map)
33
+ (map[item] ||= []) << person
34
+ end
35
+
36
+ protected
37
+
38
+ attr_reader :person, :item
39
+
40
+ private
41
+
42
+ def fetch_required_attribute(attributes, name)
43
+ attributes.fetch(name) { raise ArgumentError.new("Attribute #{name} is required") }
44
+ end
45
+ end
46
+ end
data/lib/likes/set.rb ADDED
@@ -0,0 +1,147 @@
1
+ module Likes
2
+ # Job: Understands patterns in people likings
3
+ class Set
4
+ # Creates new instance of Set
5
+ #
6
+ # @param [Array<Like>] likes List of likes
7
+ def initialize(likes)
8
+ @likes = likes
9
+ build_likes_of
10
+ build_liked
11
+ end
12
+
13
+ # Provides list of recommendations for person based on this
14
+ # likeset
15
+ #
16
+ # Should handle amount of distinct persons <= 10**6 and amount of
17
+ # distinct items <= 10**6, but likeset length is <= 10**7, ie it
18
+ # is advised to use only recent likes (couple of weeks or month)
19
+ #
20
+ # Worst approximation of execution time:
21
+ #
22
+ # Given K = how much likes target person has, in reasonable
23
+ # situations it is not very big number
24
+ #
25
+ # Given N = how much distinct persons we have
26
+ #
27
+ # Complexity: O(NK)
28
+ #
29
+ # @param [Person#==] person The person to provide recommendations
30
+ # for
31
+ # @return [Array<Item>] List of recommendations for the person
32
+ def recommendations_for(person)
33
+ Recommendations.new(person, likes_of, liked).solve
34
+ end
35
+
36
+ private
37
+
38
+ attr_accessor :likes, :likes_of, :liked
39
+
40
+ def build_likes_of
41
+ @likes_of = {}
42
+ likes.each do |like|
43
+ like.add_item_to_map(likes_of)
44
+ end
45
+ end
46
+
47
+ def build_liked
48
+ @liked = {}
49
+ likes.each do |like|
50
+ like.add_person_to_map(liked)
51
+ end
52
+ end
53
+
54
+ # @private
55
+ # Job: Understands which items could be recommended
56
+ class Recommendations
57
+ def initialize(person, likes_of, liked)
58
+ @intersections = Intersections.new(person)
59
+ @likes_of = likes_of
60
+ @liked = liked
61
+ @its_likes = likes_of.fetch(person)
62
+ add_similar_tastes
63
+ end
64
+
65
+ def solve
66
+ solution_candidate(intersections.next_people_with_similar_tastes)
67
+ end
68
+
69
+ private
70
+
71
+ attr_reader :intersections, :likes_of, :liked, :its_likes
72
+
73
+ def solution_candidate(candidates)
74
+ return [] if candidates.empty?
75
+ non_empty_solution(_solution_candidate(candidates))
76
+ end
77
+
78
+ def _solution_candidate(candidates)
79
+ candidates.map { |other_person, _|
80
+ likes_of.fetch(other_person) - its_likes
81
+ }.flatten.uniq
82
+ end
83
+
84
+ def non_empty_solution(solution)
85
+ return solve if solution.empty?
86
+ solution
87
+ end
88
+
89
+ def add_similar_tastes
90
+ its_likes.each do |item|
91
+ intersections.add_similar_tastes(liked.fetch(item))
92
+ end
93
+ end
94
+ end
95
+
96
+ # @private
97
+ # Job: Understands similar tastes
98
+ class Intersections
99
+ NO_LIMIT = Object.new.freeze
100
+
101
+ def initialize(person)
102
+ @sizes = {}
103
+ @person = person
104
+ @best_size = NO_LIMIT
105
+ end
106
+
107
+ def add_similar_tastes(people)
108
+ people.each do |other_person|
109
+ next if person == other_person
110
+ sizes[other_person] = sizes.fetch(other_person, 0) + 1
111
+ end
112
+ end
113
+
114
+ def next_people_with_similar_tastes
115
+ candidates_with(next_best_size)
116
+ end
117
+
118
+ private
119
+
120
+ attr_reader :sizes, :person
121
+
122
+ def candidates_with(intersection_size)
123
+ return [] if no_limit?(intersection_size)
124
+ sizes.select { |_, size| intersection_size == size }
125
+ end
126
+
127
+ def next_best_size
128
+ @best_size = get_best_size(@best_size)
129
+ end
130
+
131
+ def get_best_size(limit)
132
+ sizes
133
+ .select { |_, size| in_limit?(size, limit) }
134
+ .map { |_, size| size }.max || NO_LIMIT
135
+ end
136
+
137
+ def in_limit?(size, limit)
138
+ return true if no_limit?(limit)
139
+ size < limit
140
+ end
141
+
142
+ def no_limit?(limit)
143
+ NO_LIMIT == limit
144
+ end
145
+ end
146
+ end
147
+ end
data/lib/likes/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Likes
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/likes.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require "likes/version"
2
+ require "likes/like"
3
+ require "likes/set"
2
4
 
3
5
  module Likes
4
6
  # Your code goes here...
data/likes.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Likes::VERSION
9
9
  spec.authors = ["Alexey Fedorov"]
10
10
  spec.email = ["waterlink000@gmail.com"]
11
- spec.summary = %q{Give it a list of people and their likings and it will tell it what else could these people like.}
12
- spec.description = %q{Give it a list of people and their likings and it will tell it what else could these people like. Made for a greater good.}
11
+ spec.summary = %q{Give it a list of people and their likings and it will tell what else could these people like.}
12
+ spec.description = %q{Give it a list of people and their likings and it will tell what else could these people like. Made for a greater good.}
13
13
  spec.homepage = "https://github.com/waterlink/likes"
14
14
  spec.license = "MIT"
15
15
 
@@ -0,0 +1,46 @@
1
+ module Likes
2
+ RSpec.describe Like do
3
+ describe "creation" do
4
+ it "can be created with person and item" do
5
+ expect(Like.new(person: 1, item: 2)).to be_a(Like)
6
+ end
7
+
8
+ it "cannot be created without person and/or item" do
9
+ expect { Like.new(person: 1) }.to raise_error(ArgumentError, /item is required/)
10
+ expect { Like.new(item: 2) }.to raise_error(ArgumentError, /person is required/)
11
+ expect { Like.new }.to raise_error(ArgumentError, /is required/)
12
+ end
13
+
14
+ it "ignores excessive attributes" do
15
+ expect(Like.new(person: 1, item: 3, hello: "world")).to eq(Like.new(person: 1, item: 3))
16
+ end
17
+ end
18
+
19
+ describe "encapsulation" do
20
+ it "does not allow to access its data" do
21
+ expect { Like.new(person: 1, item: 2).person }.to raise_error(NoMethodError)
22
+ expect { Like.new(person: 1, item: 2).person = 3 }.to raise_error(NoMethodError)
23
+
24
+ expect { Like.new(person: 1, item: 2).item }.to raise_error(NoMethodError)
25
+ expect { Like.new(person: 1, item: 2).item = 3 }.to raise_error(NoMethodError)
26
+ end
27
+ end
28
+
29
+ describe "equality" do
30
+ it "is equal to another like with same person and item" do
31
+ expect(Like.new(person: 1, item: 2)).to eq(Like.new(person: 1, item: 2))
32
+ end
33
+
34
+ it "is not equal to another like with different person and/or item" do
35
+ expect(Like.new(person: 1, item: 2)).not_to eq(Like.new(person: 3, item: 2))
36
+ expect(Like.new(person: 1, item: 2)).not_to eq(Like.new(person: 1, item: 5))
37
+ expect(Like.new(person: 1, item: 2)).not_to eq(Like.new(person: 2, item: 4))
38
+ end
39
+
40
+ it "is not equal to different objects" do
41
+ expect(Like.new(person: 1, item: 2)).not_to eq(nil)
42
+ expect(Like.new(person: 1, item: 2)).not_to eq(Object.new)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,85 @@
1
+ module Likes
2
+ RSpec.describe Set do
3
+ let(:likeset) {
4
+ Likes::Set.new([
5
+ Likes::Like.new(person: 1, item: 1),
6
+ Likes::Like.new(person: 1, item: 5),
7
+ Likes::Like.new(person: 1, item: 4),
8
+ Likes::Like.new(person: 1, item: 7),
9
+
10
+ Likes::Like.new(person: 2, item: 1),
11
+ Likes::Like.new(person: 2, item: 3),
12
+ Likes::Like.new(person: 2, item: 5),
13
+ Likes::Like.new(person: 2, item: 4),
14
+
15
+ Likes::Like.new(person: 3, item: 3),
16
+ Likes::Like.new(person: 3, item: 2),
17
+ Likes::Like.new(person: 3, item: 5),
18
+ Likes::Like.new(person: 3, item: 4),
19
+ Likes::Like.new(person: 3, item: 9),
20
+ ])
21
+ }
22
+
23
+ describe "#recommendations_for" do
24
+ it "returns a list of items" do
25
+ expect(likeset.recommendations_for(1)).to be_a(Array)
26
+ end
27
+
28
+ it "returns right recommendations" do
29
+ expect(likeset.recommendations_for(1).sort).to eq([3])
30
+ expect(likeset.recommendations_for(2).sort).to eq([2, 7, 9])
31
+ expect(likeset.recommendations_for(3).sort).to eq([1])
32
+ end
33
+
34
+ context "when there is an identic match with other person" do
35
+ let(:likeset) {
36
+ Likes::Set.new([
37
+ Likes::Like.new(person: 1, item: 1),
38
+ Likes::Like.new(person: 1, item: 5),
39
+ Likes::Like.new(person: 1, item: 4),
40
+ Likes::Like.new(person: 1, item: 7),
41
+
42
+ Likes::Like.new(person: 2, item: 1),
43
+ Likes::Like.new(person: 2, item: 3),
44
+ Likes::Like.new(person: 2, item: 5),
45
+ Likes::Like.new(person: 2, item: 4),
46
+
47
+ Likes::Like.new(person: 4, item: 1),
48
+ Likes::Like.new(person: 4, item: 5),
49
+ Likes::Like.new(person: 4, item: 4),
50
+ Likes::Like.new(person: 4, item: 7),
51
+
52
+ Likes::Like.new(person: 3, item: 3),
53
+ Likes::Like.new(person: 3, item: 2),
54
+ Likes::Like.new(person: 3, item: 5),
55
+ Likes::Like.new(person: 3, item: 4),
56
+ Likes::Like.new(person: 3, item: 9),
57
+ ])
58
+ }
59
+
60
+ it "ignores identical candidates" do
61
+ expect(likeset.recommendations_for(1).sort).to eq([3])
62
+ end
63
+ end
64
+
65
+ context "when there is nothing to recommend" do
66
+ let(:likeset) {
67
+ Likes::Set.new([
68
+ Likes::Like.new(person: 1, item: 1),
69
+ Likes::Like.new(person: 1, item: 5),
70
+ Likes::Like.new(person: 1, item: 4),
71
+ Likes::Like.new(person: 1, item: 7),
72
+
73
+ Likes::Like.new(person: 4, item: 1),
74
+ Likes::Like.new(person: 4, item: 5),
75
+ Likes::Like.new(person: 4, item: 7),
76
+ ])
77
+ }
78
+
79
+ it "returns an empty solution" do
80
+ expect(likeset.recommendations_for(1).sort).to eq([])
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,28 @@
1
+ require "likes"
2
+
3
+ RSpec.configure do |config|
4
+ config.expect_with :rspec do |expectations|
5
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6
+ end
7
+
8
+ config.mock_with :rspec do |mocks|
9
+ mocks.verify_partial_doubles = true
10
+ end
11
+
12
+ config.filter_run :focus
13
+ config.run_all_when_everything_filtered = true
14
+
15
+ config.disable_monkey_patching!
16
+
17
+ config.warnings = true
18
+
19
+ if config.files_to_run.one?
20
+ config.default_formatter = 'doc'
21
+ end
22
+
23
+ config.profile_examples = 10
24
+
25
+ config.order = :random
26
+
27
+ Kernel.srand config.seed
28
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: likes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Fedorov
@@ -38,7 +38,7 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
- description: Give it a list of people and their likings and it will tell it what else
41
+ description: Give it a list of people and their likings and it will tell what else
42
42
  could these people like. Made for a greater good.
43
43
  email:
44
44
  - waterlink000@gmail.com
@@ -47,13 +47,19 @@ extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
49
  - ".gitignore"
50
+ - ".rspec"
50
51
  - Gemfile
51
52
  - LICENSE.txt
52
53
  - README.md
53
54
  - Rakefile
54
55
  - lib/likes.rb
56
+ - lib/likes/like.rb
57
+ - lib/likes/set.rb
55
58
  - lib/likes/version.rb
56
59
  - likes.gemspec
60
+ - spec/likes/like_spec.rb
61
+ - spec/likes/set_spec.rb
62
+ - spec/spec_helper.rb
57
63
  homepage: https://github.com/waterlink/likes
58
64
  licenses:
59
65
  - MIT
@@ -77,6 +83,10 @@ rubyforge_project:
77
83
  rubygems_version: 2.2.2
78
84
  signing_key:
79
85
  specification_version: 4
80
- summary: Give it a list of people and their likings and it will tell it what else
81
- could these people like.
82
- test_files: []
86
+ summary: Give it a list of people and their likings and it will tell what else could
87
+ these people like.
88
+ test_files:
89
+ - spec/likes/like_spec.rb
90
+ - spec/likes/set_spec.rb
91
+ - spec/spec_helper.rb
92
+ has_rdoc: