active_median 0.2.1 → 0.2.6

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
  SHA256:
3
- metadata.gz: cfbbd982f15a11c2f820533f977d7d066e1a8f46a397155681ca06673cf44cc2
4
- data.tar.gz: d2156aea58744c1d2955c00077f2b4ef064b2d541d8c69f0ca0a923d93ecbd29
3
+ metadata.gz: 97636a27f16c0024056aa067afe5d54dcb01247be3236573db0d4a5365cf72ee
4
+ data.tar.gz: 5c7dc9c490730959e05f4a633765ded663612f02543d62bb17ab05dfd1828f3e
5
5
  SHA512:
6
- metadata.gz: b5ff96c9f885f59dbc83dd1744fe427f67d1e43eefdb527fcb7e8994427452de186e12e8e0849bc1b15d3bcca2f65c0ec2a7403c631d2dba0f170b87bda074df
7
- data.tar.gz: 39671ed1587132c3e54cf9bb1e44f3d9999e7435aacdb2438d03989e94e8cb29b3448663c06bb23fb45315609098d28dc557d0726db612dc290c0229b9a2361a
6
+ metadata.gz: 48a5936c99b5325a8b7ac43035c7207912911a2e5ddd0889ccc6a4478d35360482cb2062f1756b5e0ba499e54ffff7e79e8adfe9fe29851a028d70194a497949
7
+ data.tar.gz: abb786892e89b6361e7bf107d702ac4be5e9439f1a5364a7395c490c335c286bcdbcb6c106d130a6ef0870d3b2f100d6ded7c5c69db3ab87fbef4380dca8b9ca
@@ -1,9 +1,31 @@
1
- ## 0.2.1
1
+ ## 0.2.6 (2020-12-18)
2
+
3
+ - Fixed error with certain column types for Active Record 6.1
4
+
5
+ ## 0.2.5 (2020-09-07)
6
+
7
+ - Added warning for non-attribute argument
8
+
9
+ ## 0.2.4 (2020-03-12)
10
+
11
+ - Added `percentile` method
12
+
13
+ ## 0.2.3 (2019-09-03)
14
+
15
+ - Added support for Mongoid
16
+ - Dropped support for Active Record 4.2
17
+
18
+ ## 0.2.2 (2018-10-29)
19
+
20
+ - Added support for MySQL with udf_infusion
21
+ - Added support for SQL Server and Redshift
22
+
23
+ ## 0.2.1 (2018-10-15)
2
24
 
3
25
  - Added support for arrays and hashes
4
26
  - Added compatibility with Groupdate 4
5
27
 
6
- ## 0.2.0
28
+ ## 0.2.0 (2018-10-15)
7
29
 
8
30
  - Added support for MariaDB 10.3.3+ and SQLite
9
31
  - Use `PERCENTILE_CONT` for 4x performance increase
@@ -12,23 +34,23 @@ Breaking
12
34
 
13
35
  - Dropped support for Postgres < 9.4
14
36
 
15
- ## 0.1.4
37
+ ## 0.1.4 (2016-12-03)
16
38
 
17
39
  - Added `drop_function` method
18
40
 
19
- ## 0.1.3
41
+ ## 0.1.3 (2016-03-22)
20
42
 
21
- - Added support for ActiveRecord 5.0
43
+ - Added support for Active Record 5.0
22
44
 
23
- ## 0.1.2
45
+ ## 0.1.2 (2014-12-27)
24
46
 
25
- - Added support for ActiveRecord 4.2
47
+ - Added support for Active Record 4.2
26
48
 
27
- ## 0.1.1
49
+ ## 0.1.1 (2014-08-12)
28
50
 
29
51
  - 10x faster median
30
52
  - Added tests
31
53
 
32
- ## 0.1.0
54
+ ## 0.1.0 (2014-03-13)
33
55
 
34
56
  - First release
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Andrew Kane
1
+ Copyright (c) 2013-2020 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,35 +1,47 @@
1
1
  # ActiveMedian
2
2
 
3
- Median for ActiveRecord
3
+ Median and percentile for Active Record, Mongoid, arrays, and hashes
4
4
 
5
- Supports PostgreSQL 9.4+, MariaDB 10.3.3+, and SQLite, plus arrays and hashes
5
+ Supports:
6
+
7
+ - PostgreSQL 9.4+
8
+ - MariaDB 10.3.3+
9
+ - MySQL and SQL (with extensions)
10
+ - SQL Server 2012+
11
+ - MongoDB 2.1+
6
12
 
7
13
  :fire: Uses native functions for blazing performance
8
14
 
9
- [![Build Status](https://travis-ci.org/ankane/active_median.svg)](https://travis-ci.org/ankane/active_median)
15
+ [![Build Status](https://github.com/ankane/active_median/workflows/build/badge.svg?branch=master)](https://github.com/ankane/active_median/actions)
16
+
17
+ ## Getting Started
10
18
 
11
- ## Usage
19
+ Add this line to your application’s Gemfile:
12
20
 
13
21
  ```ruby
14
- Item.median(:price)
22
+ gem 'active_median'
15
23
  ```
16
24
 
17
- Works with grouping, too.
25
+ For MySQL and SQLite, also follow [these instructions](#additional-instructions).
26
+
27
+ ## Models
28
+
29
+ Median
18
30
 
19
31
  ```ruby
20
- Order.group(:store_id).median(:total)
32
+ Item.median(:price)
21
33
  ```
22
34
 
23
- ## Arrays and Hashes
35
+ Percentile
24
36
 
25
37
  ```ruby
26
- [1, 2, 3].median
38
+ Request.percentile(:response_time, 0.95)
27
39
  ```
28
40
 
29
- You can also pass a block.
41
+ Works with grouping, too
30
42
 
31
43
  ```ruby
32
- {a: 1, b: 2, c: 3}.median { |k, v| v }
44
+ Order.group(:store_id).median(:total)
33
45
  ```
34
46
 
35
47
  ## User Input
@@ -45,15 +57,40 @@ raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
45
57
  User.median(column)
46
58
  ```
47
59
 
48
- ## Installation
60
+ ## Arrays and Hashes
49
61
 
50
- Add this line to your application’s Gemfile:
62
+ Median
51
63
 
52
64
  ```ruby
53
- gem 'active_median'
65
+ [1, 2, 3].median
54
66
  ```
55
67
 
56
- For SQLite, also follow the instructions below.
68
+ Percentile
69
+
70
+ ```ruby
71
+ [1, 2, 3].percentile(0.95)
72
+ ```
73
+
74
+ You can also pass a block
75
+
76
+ ```ruby
77
+ {a: 1, b: 2, c: 3}.median { |k, v| v }
78
+ ```
79
+
80
+ ## Additional Instructions
81
+
82
+ ### MySQL
83
+
84
+ MySQL requires the `PERCENTILE_CONT` function from [udf_infusion](https://github.com/infusion/udf_infusion). To install it, do:
85
+
86
+ ```sh
87
+ git clone https://github.com/infusion/udf_infusion.git
88
+ cd udf_infusion
89
+ ./configure --enable-functions="percentile_cont"
90
+ make
91
+ sudo make install
92
+ mysql <options> < load.sql
93
+ ```
57
94
 
58
95
  ### SQLite
59
96
 
@@ -76,7 +113,7 @@ db.enable_load_extension(0)
76
113
 
77
114
  ### 0.2.0
78
115
 
79
- A user-defined function is no longer needed. Create a migration with `ActiveMedian.drop_function` to remove it.
116
+ A user-defined function is no longer needed for Postgres. Create a migration with `ActiveMedian.drop_function` to remove it.
80
117
 
81
118
  ## Contributing
82
119
 
@@ -86,3 +123,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
86
123
  - Fix bugs and [submit pull requests](https://github.com/ankane/active_median/pulls)
87
124
  - Write, clarify, or fix documentation
88
125
  - Suggest or add new features
126
+
127
+ To get started with development and testing:
128
+
129
+ ```sh
130
+ git clone https://github.com/ankane/active_median.git
131
+ cd active_median
132
+ createdb active_median_test
133
+ bundle install
134
+ bundle exec rake test
135
+ ```
@@ -1,6 +1,5 @@
1
1
  require "active_support"
2
2
 
3
- require "active_median/model"
4
3
  require "active_median/enumerable"
5
4
  require "active_median/version"
6
5
 
@@ -15,5 +14,11 @@ module ActiveMedian
15
14
  end
16
15
 
17
16
  ActiveSupport.on_load(:active_record) do
17
+ require "active_median/model"
18
18
  extend(ActiveMedian::Model)
19
19
  end
20
+
21
+ ActiveSupport.on_load(:mongoid) do
22
+ require "active_median/mongoid"
23
+ Mongoid::Document::ClassMethods.include(ActiveMedian::Mongoid)
24
+ end
@@ -1,12 +1,40 @@
1
1
  module Enumerable
2
2
  unless method_defined?(:median)
3
3
  def median(*args, &block)
4
- if respond_to?(:scoping) && !block
4
+ if !block && respond_to?(:scoping)
5
5
  scoping { @klass.median(*args) }
6
+ elsif !block && respond_to?(:with_scope)
7
+ with_scope(self) { klass.median(*args) }
6
8
  else
9
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" if args.any?
10
+ percentile(0.5, &block)
11
+ end
12
+ end
13
+ end
14
+
15
+ unless method_defined?(:percentile)
16
+ def percentile(*args, &block)
17
+ if !block && respond_to?(:scoping)
18
+ scoping { @klass.percentile(*args) }
19
+ elsif !block && respond_to?(:with_scope)
20
+ with_scope(self) { klass.percentile(*args) }
21
+ else
22
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size != 1
23
+
24
+ percentile = args[0].to_f
25
+ raise ArgumentError, "percentile is not between 0 and 1" if percentile < 0 || percentile > 1
26
+
27
+ # uses C=1 variant, like percentile_cont
28
+ # https://en.wikipedia.org/wiki/Percentile#The_linear_interpolation_between_closest_ranks_method
7
29
  sorted = map(&block).sort
8
- len = sorted.length
9
- (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
30
+ x = percentile * (sorted.size - 1)
31
+ r = x % 1
32
+ i = x.floor
33
+ if i == sorted.size - 1
34
+ sorted[-1]
35
+ else
36
+ sorted[i] + r * (sorted[i + 1] - sorted[i])
37
+ end
10
38
  end
11
39
  end
12
40
  end
@@ -1,20 +1,69 @@
1
1
  module ActiveMedian
2
2
  module Model
3
3
  def median(column)
4
+ percentile(column, 0.5)
5
+ end
6
+
7
+ def percentile(column, percentile)
8
+ percentile = percentile.to_f
9
+ raise ArgumentError, "percentile is not between 0 and 1" if percentile < 0 || percentile > 1
10
+
11
+ # basic version of Active Record disallow_raw_sql!
12
+ # symbol = column (safe), Arel node = SQL (safe), other = untrusted
13
+ # matches table.column and column
14
+ unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral) || /\A\w+(\.\w+)?\z/i.match(column.to_s)
15
+ warn "[active_median] Non-attribute argument: #{column}. Use Arel.sql() for known-safe values. This will raise an error in ActiveMedian 0.3.0"
16
+ end
17
+
18
+ # column resolution
19
+ node = relation.send(:arel_columns, [column]).first
20
+ node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
21
+ column = relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
22
+
23
+ # prevent SQL injection
24
+ percentile = connection.quote(percentile)
25
+
4
26
  group_values = all.group_values
5
27
 
6
28
  relation =
7
29
  case connection.adapter_name
8
30
  when /mysql/i
31
+ # assume mariadb by default
32
+ # use send as this method is private in Rails 4.2
33
+ mariadb = connection.send(:mariadb?) rescue true
34
+
35
+ if mariadb
36
+ if group_values.any?
37
+ over = "PARTITION BY #{group_values.join(", ")}"
38
+ end
39
+
40
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
41
+ else
42
+ # if mysql gets native function, check (and memoize) version first
43
+ select(*group_values, "PERCENTILE_CONT(#{column}, #{percentile})")
44
+ end
45
+ when /sqlserver/i
9
46
  if group_values.any?
10
47
  over = "PARTITION BY #{group_values.join(", ")}"
11
48
  end
12
49
 
13
- select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
50
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
14
51
  when /sqlite/i
15
- select(*group_values, "MEDIAN(#{column})")
52
+ case percentile.to_f
53
+ when 0
54
+ select(*group_values, "MIN(#{column})")
55
+ when 0.5
56
+ select(*group_values, "MEDIAN(#{column})")
57
+ when 1
58
+ select(*group_values, "MAX(#{column})")
59
+ else
60
+ # LOWER_QUARTILE and UPPER_QUARTILE use different calculation than 0.25 and 0.75
61
+ raise "SQLite only supports 0, 0.5, and 1 percentiles"
62
+ end
63
+ when /postg/i, /redshift/i # postgis too
64
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column})")
16
65
  else
17
- select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column})")
66
+ raise "Connection adapter not supported: #{connection.adapter_name}"
18
67
  end
19
68
 
20
69
  result = connection.select_all(relation.to_sql)
@@ -22,9 +71,8 @@ module ActiveMedian
22
71
  # typecast
23
72
  rows = []
24
73
  columns = result.columns
25
- cast_method = ActiveRecord::VERSION::MAJOR < 5 ? :type_cast : :cast_value
26
74
  result.rows.each do |untyped_row|
27
- rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] ? result.column_types[c].send(cast_method, untyped_row[i]) : untyped_row[i] })
75
+ rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] && result.column_types[c] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
28
76
  end
29
77
 
30
78
  result =
@@ -0,0 +1,30 @@
1
+ module ActiveMedian
2
+ module Mongoid
3
+ def median(column)
4
+ percentile(column, 0.5)
5
+ end
6
+
7
+ # https://www.compose.com/articles/mongo-metrics-finding-a-happy-median/
8
+ def percentile(column, percentile)
9
+ percentile = percentile.to_f
10
+ raise ArgumentError, "percentile is not between 0 and 1" if percentile < 0 || percentile > 1
11
+
12
+ relation =
13
+ all
14
+ .asc(column)
15
+ .group(_id: nil, values: {"$push" => "$#{column}"}, count: {"$sum" => 1})
16
+ .project(values: 1, count: {"$subtract" => ["$count", 1]})
17
+ .project(values: 1, count: 1, x: {"$multiply" => ["$count", percentile]})
18
+ .project(values: 1, count: 1, r: {"$mod" => ["$x", 1]}, i: {"$floor" => "$x"})
19
+ .project(values: 1, count: 1, r: 1, i: 1, i2: {"$add" => ["$i", 1]})
20
+ .project(values: 1, count: 1, r: 1, i: 1, i2: {"$min" => ["$i2", "$count"]})
21
+ .project(r: 1, beginValue: {"$arrayElemAt" => ["$values", "$i"]}, endValue: {"$arrayElemAt" => ["$values", "$i2"]})
22
+ .project(r: 1, beginValue: 1, result: {"$subtract" => ["$endValue", "$beginValue"]})
23
+ .project(beginValue: 1, result: {"$multiply" => ["$result", "$r"]})
24
+ .project(result: {"$add" => ["$beginValue", "$result"]})
25
+
26
+ res = collection.aggregate(relation.pipeline).first
27
+ res ? res["result"] : nil
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveMedian
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.6"
3
3
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_median
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-15 00:00:00.000000000 Z
11
+ date: 2020-12-19 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: '4.2'
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: '4.2'
26
+ version: '5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -56,16 +56,16 @@ dependencies:
56
56
  name: pg
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "<"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '1'
61
+ version: '0'
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: '1'
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: groupdate
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -94,7 +108,7 @@ dependencies:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
97
- description:
111
+ description:
98
112
  email: andrew@chartkick.com
99
113
  executables: []
100
114
  extensions: []
@@ -106,12 +120,13 @@ files:
106
120
  - lib/active_median.rb
107
121
  - lib/active_median/enumerable.rb
108
122
  - lib/active_median/model.rb
123
+ - lib/active_median/mongoid.rb
109
124
  - lib/active_median/version.rb
110
125
  homepage: https://github.com/ankane/active_median
111
126
  licenses:
112
127
  - MIT
113
128
  metadata: {}
114
- post_install_message:
129
+ post_install_message:
115
130
  rdoc_options: []
116
131
  require_paths:
117
132
  - lib
@@ -119,16 +134,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
119
134
  requirements:
120
135
  - - ">="
121
136
  - !ruby/object:Gem::Version
122
- version: '2.2'
137
+ version: '2.4'
123
138
  required_rubygems_version: !ruby/object:Gem::Requirement
124
139
  requirements:
125
140
  - - ">="
126
141
  - !ruby/object:Gem::Version
127
142
  version: '0'
128
143
  requirements: []
129
- rubyforge_project:
130
- rubygems_version: 2.7.7
131
- signing_key:
144
+ rubygems_version: 3.1.4
145
+ signing_key:
132
146
  specification_version: 4
133
- summary: Median for ActiveRecord
147
+ summary: Median and percentile for Active Record, Mongoid, arrays, and hashes
134
148
  test_files: []