likes 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: