hightop 0.1.3 → 0.2.3

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
- SHA1:
3
- metadata.gz: 8123384a25f9687e070f4ee86677c1afc5060a1f
4
- data.tar.gz: 3b3776446331302a639859ba54d12cfa77b450aa
2
+ SHA256:
3
+ metadata.gz: 340c609cb491de09f5c69f80b3e237126c081a8167565c78d6f28cac692cc34b
4
+ data.tar.gz: 0aa4d15e3ac6503a73f664602d0d07f1a0e956202f93411ec51d663dcca9b2ff
5
5
  SHA512:
6
- metadata.gz: 51c28c43483583167675608a90698f5a3e5b2b7bcf362278ab89b63a56c065d7adad51993e111005201b70e2e63ae5f7570db5e5eb1ce39cec1df085f32952ad
7
- data.tar.gz: 191b537904fcb730a4e313714f9168b475e8017a791131e60c9173ea4ec5c7a5e817e73324af5388c0161a106eb336e20da3b4a8e6074f2ed5bcc81db0d2dd8c
6
+ metadata.gz: 31bcc5dcc655886f5dad2ebe27f33a7f56702b0503883bac779a944b502dc9bf1f27694c1eb83e4d36ca908fbb1bbe5e9b09706b5536f1ba53616e555204c579
7
+ data.tar.gz: 1e4d59093c22c49d3166368d8b23386ca7366dd9fc1b31e813e9df81bdd5d3b3e7d87ece4d36bf6b94deef69d2cd093927c0d9eeb62582fd357feed0c381564d
@@ -1,33 +1,54 @@
1
- ## 0.1.3
1
+ ## 0.2.3 (2020-06-18)
2
+
3
+ - Dropped support for Rails 4.2 and Ruby 2.3
4
+ - Fixed deprecation warning in Ruby 2.7
5
+
6
+ ## 0.2.2 (2019-08-12)
7
+
8
+ - Added support for Mongoid
9
+
10
+ ## 0.2.1 (2019-08-04)
11
+
12
+ - Added support for arrays and hashes
13
+
14
+ ## 0.2.0 (2017-03-19)
15
+
16
+ - Use keyword arguments
17
+
18
+ ## 0.1.4 (2016-02-04)
19
+
20
+ - Added `distinct` option to replace `uniq`
21
+
22
+ ## 0.1.3 (2015-06-18)
2
23
 
3
24
  - Fixed `min` option with `uniq`
4
25
 
5
- ## 0.1.2
26
+ ## 0.1.2 (2014-11-05)
6
27
 
7
28
  - Added `min` option
8
29
 
9
- ## 0.1.1
30
+ ## 0.1.1 (2014-07-02)
10
31
 
11
32
  - Added `uniq` option
12
33
  - Fixed `Model.limit(n).top`
13
34
 
14
- ## 0.1.0
35
+ ## 0.1.0 (2014-06-11)
15
36
 
16
37
  - No changes, just bump
17
38
 
18
- ## 0.0.4
39
+ ## 0.0.4 (2014-06-11)
19
40
 
20
41
  - Added support for multiple groups
21
42
  - Added `nil` option
22
43
 
23
- ## 0.0.3
44
+ ## 0.0.3 (2014-06-11)
24
45
 
25
46
  - Fixed escaping
26
47
 
27
- ## 0.0.2
48
+ ## 0.0.2 (2014-05-29)
28
49
 
29
50
  - Added `limit` parameter
30
51
 
31
- ## 0.0.1
52
+ ## 0.0.1 (2014-05-11)
32
53
 
33
54
  - First release
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Andrew Kane
1
+ Copyright (c) 2014-2020 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -2,19 +2,28 @@
2
2
 
3
3
  A nice shortcut for group count queries
4
4
 
5
- [![Build Status](https://travis-ci.org/ankane/hightop.svg)](https://travis-ci.org/ankane/hightop)
6
-
7
5
  ```ruby
8
6
  Visit.top(:browser)
7
+ # {
8
+ # "Chrome" => 63,
9
+ # "Safari" => 50,
10
+ # "Firefox" => 34
11
+ # }
9
12
  ```
10
13
 
11
- instead of
14
+ Works with Active Record, Mongoid, arrays and hashes
15
+
16
+ [![Build Status](https://travis-ci.org/ankane/hightop.svg?branch=master)](https://travis-ci.org/ankane/hightop)
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application’s Gemfile:
12
21
 
13
22
  ```ruby
14
- Visit.group(:browser).where("browser IS NOT NULL").order("count_all DESC, browser").count
23
+ gem 'hightop'
15
24
  ```
16
25
 
17
- Be sure to [sanitize user input](http://rails-sqli.org/) like you must with `group`.
26
+ ## Options
18
27
 
19
28
  Limit the results
20
29
 
@@ -37,13 +46,13 @@ Visit.top([:city, :browser])
37
46
  And expressions
38
47
 
39
48
  ```ruby
40
- Visit.top("LOWER(referring_domain)")
49
+ Visit.top(Arel.sql("LOWER(referring_domain)"))
41
50
  ```
42
51
 
43
52
  And distinct
44
53
 
45
54
  ```ruby
46
- Visit.top(:city, uniq: :user_id)
55
+ Visit.top(:city, distinct: :user_id)
47
56
  ```
48
57
 
49
58
  And min count
@@ -52,18 +61,49 @@ And min count
52
61
  Visit.top(:city, min: 10)
53
62
  ```
54
63
 
55
- ## Installation
64
+ ## User Input
56
65
 
57
- Add this line to your application’s Gemfile:
66
+ If passing user input as the column, be sure to sanitize it first [like you must](https://rails-sqli.org/) with `group`.
58
67
 
59
68
  ```ruby
60
- gem 'hightop'
69
+ column = params[:column]
70
+
71
+ # check against permitted columns
72
+ raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
73
+
74
+ User.top(column)
61
75
  ```
62
76
 
63
- And then execute:
77
+ ## Arrays and Hashes
64
78
 
65
- ```sh
66
- bundle
79
+ Arrays
80
+
81
+ ```ruby
82
+ ["up", "up", "down"].top
83
+ ```
84
+
85
+ Hashes
86
+
87
+ ```ruby
88
+ {a: "up", b: "up", c: "down"}.top { |k, v| v }
89
+ ```
90
+
91
+ Limit the results
92
+
93
+ ```ruby
94
+ ["up", "up", "down"].top(1)
95
+ ```
96
+
97
+ Include nil values
98
+
99
+ ```ruby
100
+ [nil, nil, "down"].top(nil: true)
101
+ ```
102
+
103
+ Min count
104
+
105
+ ```ruby
106
+ ["up", "up", "down"].top(min: 2)
67
107
  ```
68
108
 
69
109
  ## History
@@ -78,3 +118,12 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
78
118
  - Fix bugs and [submit pull requests](https://github.com/ankane/hightop/pulls)
79
119
  - Write, clarify, or fix documentation
80
120
  - Suggest or add new features
121
+
122
+ To get started with development and testing:
123
+
124
+ ```sh
125
+ git clone https://github.com/ankane/hightop.git
126
+ cd hightop
127
+ bundle install
128
+ bundle exec rake test
129
+ ```
@@ -1,35 +1,16 @@
1
- require "hightop/version"
2
- require "active_record"
3
-
4
- module Hightop
5
- def top(column, limit = nil, options = {})
6
- if limit.is_a?(Hash)
7
- options = limit
8
- limit = nil
9
- end
10
-
11
- order_str = column.is_a?(Array) ? column.map(&:to_s).join(", ") : column
12
- relation = group(column).order("count_#{options[:uniq] || 'all'} DESC, #{order_str}")
13
- if limit
14
- relation = relation.limit(limit)
15
- end
1
+ # dependencies
2
+ require "active_support"
16
3
 
17
- unless options[:nil]
18
- (column.is_a?(Array) ? column : [column]).each do |c|
19
- relation = relation.where("#{c} IS NOT NULL")
20
- end
21
- end
22
-
23
- if options[:min]
24
- relation = relation.having("COUNT(#{options[:uniq] ? "DISTINCT #{options[:uniq]}" : '*'}) >= #{options[:min].to_i}")
25
- end
4
+ # modules
5
+ require "hightop/enumerable"
6
+ require "hightop/version"
26
7
 
27
- if options[:uniq]
28
- relation.uniq.count(options[:uniq])
29
- else
30
- relation.count
31
- end
32
- end
8
+ ActiveSupport.on_load(:active_record) do
9
+ require "hightop/kicks"
10
+ extend Hightop::Kicks
33
11
  end
34
12
 
35
- ActiveRecord::Base.send :extend, Hightop
13
+ ActiveSupport.on_load(:mongoid) do
14
+ require "hightop/mongoid"
15
+ Mongoid::Document::ClassMethods.include(Hightop::Mongoid)
16
+ end
@@ -0,0 +1,24 @@
1
+ module Enumerable
2
+ def top(*args, **options, &block)
3
+ if block || !(respond_to?(:scoping) || respond_to?(:with_scope))
4
+ # TODO raise error if too many arguments
5
+ limit = args[0]
6
+ min = options[:min]
7
+
8
+ counts = Hash.new(0)
9
+ map(&block).each do |v|
10
+ counts[v] += 1
11
+ end
12
+ counts.delete(nil) unless options[:nil]
13
+ counts.select! { |_, v| v >= min } if min
14
+
15
+ arr = counts.sort_by { |_, v| -v }
16
+ arr = arr[0...limit] if limit
17
+ Hash[arr]
18
+ elsif respond_to?(:scoping)
19
+ scoping { @klass.send(:top, *args, **options, &block) }
20
+ else
21
+ with_scope(self) { klass.send(:top, *args, **options, &block) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module Hightop
2
+ module Kicks
3
+ def top(column, limit = nil, distinct: nil, uniq: nil, min: nil, nil: nil)
4
+ distinct ||= uniq
5
+ order_str = column.is_a?(Array) ? column.map(&:to_s).join(", ") : column
6
+ relation = group(column).order(["count_#{distinct || 'all'} DESC", order_str])
7
+ if limit
8
+ relation = relation.limit(limit)
9
+ end
10
+
11
+ # terribly named option
12
+ unless binding.local_variable_get(:nil)
13
+ (column.is_a?(Array) ? column : [column]).each do |c|
14
+ relation = relation.where("#{c} IS NOT NULL")
15
+ end
16
+ end
17
+
18
+ if min
19
+ relation = relation.having("COUNT(#{distinct ? "DISTINCT #{distinct}" : '*'}) >= #{min.to_i}")
20
+ end
21
+
22
+ if distinct
23
+ relation.distinct.count(distinct)
24
+ else
25
+ relation.count
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ module Hightop
2
+ module Mongoid
3
+ # super helpful article
4
+ # https://maximomussini.com/posts/mongoid-aggregation-dsl/
5
+ def top(column, limit = nil, distinct: nil, uniq: nil, min: nil, nil: nil)
6
+ distinct ||= uniq
7
+
8
+ relation = all
9
+
10
+ # terribly named option
11
+ unless binding.local_variable_get(:nil)
12
+ (column.is_a?(Array) ? column : [column]).each do |c|
13
+ relation = relation.and(c.ne => nil)
14
+ end
15
+ end
16
+
17
+ ids = {}
18
+ Array(column).each_with_index do |c, i|
19
+ ids["c#{i}"] = "$#{c}"
20
+ end
21
+
22
+ if distinct
23
+ # group with distinct column first, then group without it
24
+ # https://stackoverflow.com/questions/24761266/select-group-by-count-and-distinct-count-in-same-mongodb-query/24770233#24770233
25
+ distinct_ids = ids.merge("c#{ids.size}" => "$#{distinct}")
26
+ relation = relation.group(_id: distinct_ids, count: {"$sum" => 1})
27
+ ids.each_key do |k|
28
+ ids[k] = "$_id.#{k}"
29
+ end
30
+ end
31
+
32
+ relation = relation.group(_id: ids, count: {"$sum" => 1})
33
+
34
+ if min
35
+ relation.pipeline.push("$match" => {"count" => {"$gte" => min}})
36
+ end
37
+
38
+ relation = relation.desc(:count)
39
+ if limit
40
+ relation = relation.limit(limit)
41
+ end
42
+
43
+ result = {}
44
+ collection.aggregate(relation.pipeline).each do |doc|
45
+ key = doc["_id"].values
46
+ key = key[0] if key.size == 1
47
+ result[key] = doc["count"]
48
+ end
49
+ result
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module Hightop
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.3"
3
3
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hightop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-18 00:00:00.000000000 Z
11
+ date: 2020-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '5'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.6'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.6'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '5'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '5'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: sqlite3
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,25 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- description: A nice shortcut for group count queries
84
- email:
85
- - andrew@chartkick.com
83
+ description:
84
+ email: andrew@chartkick.com
86
85
  executables: []
87
86
  extensions: []
88
87
  extra_rdoc_files: []
89
88
  files:
90
- - ".gitignore"
91
- - ".travis.yml"
92
89
  - CHANGELOG.md
93
- - Gemfile
94
90
  - LICENSE.txt
95
91
  - README.md
96
- - Rakefile
97
- - hightop.gemspec
98
92
  - lib/hightop.rb
93
+ - lib/hightop/enumerable.rb
94
+ - lib/hightop/kicks.rb
95
+ - lib/hightop/mongoid.rb
99
96
  - lib/hightop/version.rb
100
- - test/hightop_test.rb
101
- - test/test_helper.rb
102
97
  homepage: https://github.com/ankane/hightop
103
98
  licenses:
104
99
  - MIT
@@ -111,19 +106,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
106
  requirements:
112
107
  - - ">="
113
108
  - !ruby/object:Gem::Version
114
- version: '0'
109
+ version: '2.4'
115
110
  required_rubygems_version: !ruby/object:Gem::Requirement
116
111
  requirements:
117
112
  - - ">="
118
113
  - !ruby/object:Gem::Version
119
114
  version: '0'
120
115
  requirements: []
121
- rubyforge_project:
122
- rubygems_version: 2.4.5
116
+ rubygems_version: 3.1.2
123
117
  signing_key:
124
118
  specification_version: 4
125
119
  summary: A nice shortcut for group count queries
126
- test_files:
127
- - test/hightop_test.rb
128
- - test/test_helper.rb
129
- has_rdoc:
120
+ test_files: []
data/.gitignore DELETED
@@ -1,22 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
18
- *.bundle
19
- *.so
20
- *.o
21
- *.a
22
- mkmf.log
@@ -1,10 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.1
4
- gemfile:
5
- - Gemfile
6
- script: bundle exec rake test
7
- notifications:
8
- email:
9
- on_success: never
10
- on_failure: change
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in hightop.gemspec
4
- gemspec
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- task default: :test
5
- Rake::TestTask.new do |t|
6
- t.libs << "test"
7
- t.pattern = "test/**/*_test.rb"
8
- end
@@ -1,27 +0,0 @@
1
- # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "hightop/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "hightop"
8
- spec.version = Hightop::VERSION
9
- spec.authors = ["Andrew Kane"]
10
- spec.email = ["andrew@chartkick.com"]
11
- spec.summary = "A nice shortcut for group count queries"
12
- spec.description = "A nice shortcut for group count queries"
13
- spec.homepage = "https://github.com/ankane/hightop"
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_dependency "activerecord"
22
-
23
- spec.add_development_dependency "bundler", "~> 1.6"
24
- spec.add_development_dependency "rake"
25
- spec.add_development_dependency "minitest"
26
- spec.add_development_dependency "sqlite3"
27
- end
@@ -1,90 +0,0 @@
1
- require_relative "test_helper"
2
-
3
- class TestHightop < Minitest::Test
4
- def setup
5
- Visit.delete_all
6
- end
7
-
8
- def test_top
9
- create_city("San Francisco", 3)
10
- create_city("Chicago", 2)
11
- expected = {
12
- "San Francisco" => 3,
13
- "Chicago" => 2
14
- }
15
- assert_equal expected, Visit.top(:city)
16
- end
17
-
18
- def test_limit
19
- create_city("San Francisco", 3)
20
- create_city("Chicago", 2)
21
- create_city("Boston", 1)
22
- expected = {
23
- "San Francisco" => 3,
24
- "Chicago" => 2
25
- }
26
- assert_equal expected, Visit.top(:city, 2)
27
- assert_equal expected, Visit.limit(2).top(:city)
28
- end
29
-
30
- def test_nil_values
31
- create_city("San Francisco", 3)
32
- create_city(nil, 2)
33
- expected = {
34
- "San Francisco" => 3
35
- }
36
- assert_equal expected, Visit.top(:city)
37
- end
38
-
39
- def test_nil_option
40
- create_city("San Francisco", 3)
41
- create_city(nil, 2)
42
- expected = {
43
- "San Francisco" => 3,
44
- nil => 2
45
- }
46
- assert_equal expected, Visit.top(:city, nil: true)
47
- end
48
-
49
- def test_multiple_groups
50
- create_city("San Francisco")
51
- expected = {
52
- ["San Francisco", "San Francisco"] => 1
53
- }
54
- assert_equal expected, Visit.top([:city, :city])
55
- end
56
-
57
- def test_expressions
58
- create_city("San Francisco")
59
- expected = {
60
- "san francisco" => 1
61
- }
62
- assert_equal expected, Visit.top("LOWER(city)")
63
- end
64
-
65
- def test_uniq
66
- create(city: "San Francisco", user_id: 1)
67
- create(city: "San Francisco", user_id: 1)
68
- expected = {
69
- "San Francisco" => 1
70
- }
71
- assert_equal expected, Visit.top(:city, uniq: :user_id)
72
- end
73
-
74
- def test_min
75
- create_city("San Francisco", 3)
76
- create_city("Chicago", 2)
77
- expected = {
78
- "San Francisco" => 3
79
- }
80
- assert_equal expected, Visit.top(:city, min: 3)
81
- end
82
-
83
- def create_city(city, count = 1)
84
- create({city: city}, count)
85
- end
86
-
87
- def create(attributes, count = 1)
88
- count.times { Visit.create!(attributes) }
89
- end
90
- end
@@ -1,19 +0,0 @@
1
- require "bundler/setup"
2
- Bundler.require(:default)
3
- require "minitest/autorun"
4
- require "minitest/pride"
5
- require "logger"
6
-
7
- # for debugging
8
- # ActiveRecord::Base.logger = Logger.new(STDOUT)
9
-
10
- # migrations
11
- ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
12
-
13
- ActiveRecord::Migration.create_table :visits do |t|
14
- t.string :city
15
- t.string :user_id
16
- end
17
-
18
- class Visit < ActiveRecord::Base
19
- end