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 +4 -4
- data/CHANGELOG.md +31 -9
- data/LICENSE.txt +1 -1
- data/README.md +63 -16
- data/lib/active_median.rb +6 -1
- data/lib/active_median/enumerable.rb +31 -3
- data/lib/active_median/model.rb +53 -5
- data/lib/active_median/mongoid.rb +30 -0
- data/lib/active_median/version.rb +1 -1
- metadata +31 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97636a27f16c0024056aa067afe5d54dcb01247be3236573db0d4a5365cf72ee
|
4
|
+
data.tar.gz: 5c7dc9c490730959e05f4a633765ded663612f02543d62bb17ab05dfd1828f3e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48a5936c99b5325a8b7ac43035c7207912911a2e5ddd0889ccc6a4478d35360482cb2062f1756b5e0ba499e54ffff7e79e8adfe9fe29851a028d70194a497949
|
7
|
+
data.tar.gz: abb786892e89b6361e7bf107d702ac4be5e9439f1a5364a7395c490c335c286bcdbcb6c106d130a6ef0870d3b2f100d6ded7c5c69db3ab87fbef4380dca8b9ca
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,31 @@
|
|
1
|
-
## 0.2.
|
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
|
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
|
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
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,35 +1,47 @@
|
|
1
1
|
# ActiveMedian
|
2
2
|
|
3
|
-
Median for
|
3
|
+
Median and percentile for Active Record, Mongoid, arrays, and hashes
|
4
4
|
|
5
|
-
Supports
|
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://
|
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
|
-
|
19
|
+
Add this line to your application’s Gemfile:
|
12
20
|
|
13
21
|
```ruby
|
14
|
-
|
22
|
+
gem 'active_median'
|
15
23
|
```
|
16
24
|
|
17
|
-
|
25
|
+
For MySQL and SQLite, also follow [these instructions](#additional-instructions).
|
26
|
+
|
27
|
+
## Models
|
28
|
+
|
29
|
+
Median
|
18
30
|
|
19
31
|
```ruby
|
20
|
-
|
32
|
+
Item.median(:price)
|
21
33
|
```
|
22
34
|
|
23
|
-
|
35
|
+
Percentile
|
24
36
|
|
25
37
|
```ruby
|
26
|
-
|
38
|
+
Request.percentile(:response_time, 0.95)
|
27
39
|
```
|
28
40
|
|
29
|
-
|
41
|
+
Works with grouping, too
|
30
42
|
|
31
43
|
```ruby
|
32
|
-
|
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
|
-
##
|
60
|
+
## Arrays and Hashes
|
49
61
|
|
50
|
-
|
62
|
+
Median
|
51
63
|
|
52
64
|
```ruby
|
53
|
-
|
65
|
+
[1, 2, 3].median
|
54
66
|
```
|
55
67
|
|
56
|
-
|
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
|
+
```
|
data/lib/active_median.rb
CHANGED
@@ -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)
|
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
|
-
|
9
|
-
|
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
|
data/lib/active_median/model.rb
CHANGED
@@ -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(
|
50
|
+
select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
|
14
51
|
when /sqlite/i
|
15
|
-
|
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
|
-
|
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(
|
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
|
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.
|
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:
|
11
|
+
date: 2020-12-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
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: '
|
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: '
|
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: '
|
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.
|
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
|
-
|
130
|
-
|
131
|
-
signing_key:
|
144
|
+
rubygems_version: 3.1.4
|
145
|
+
signing_key:
|
132
146
|
specification_version: 4
|
133
|
-
summary: Median for
|
147
|
+
summary: Median and percentile for Active Record, Mongoid, arrays, and hashes
|
134
148
|
test_files: []
|