hightop 0.1.3 → 0.2.3

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
- 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