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 +4 -4
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/README.md +3 -3
- data/lib/likes/like.rb +46 -0
- data/lib/likes/set.rb +147 -0
- data/lib/likes/version.rb +1 -1
- data/lib/likes.rb +2 -0
- data/likes.gemspec +2 -2
- data/spec/likes/like_spec.rb +46 -0
- data/spec/likes/set_spec.rb +85 -0
- data/spec/spec_helper.rb +28 -0
- metadata +15 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6148191736d05b2de8dce7c05ad3ec5781a70735
|
4
|
+
data.tar.gz: fdc47aec630718afa84d9c79f25e7a82bf71e8e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f3b0458b662807a8ba0558be63846421a8d95a1bb0d64729419aeb08b58d0f6dfafa0fe4ff91fd3fae464833152d7ef8827fdea901106bf69dc360d0bee7033
|
7
|
+
data.tar.gz: 6e8d085e9022c1ead5877416f48b250bcd2036e62ce442d345156556bb2cfc461961efecf78cc0a89f181511983b071783f29087b20df26fd91a4f0726b37c8d
|
data/.rspec
ADDED
data/Gemfile
CHANGED
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
|
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
data/lib/likes.rb
CHANGED
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
|
12
|
-
spec.description = %q{Give it a list of people and their likings and it will tell
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
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
|
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
|
81
|
-
|
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:
|