active_median 0.2.3 → 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -12
- data/LICENSE.txt +1 -1
- data/README.md +39 -12
- data/lib/active_median/enumerable.rb +28 -2
- data/lib/active_median/model.rb +41 -6
- data/lib/active_median/mongoid.rb +20 -11
- data/lib/active_median/version.rb +1 -1
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f779fe5268fc1347cd94c47fc55a3322a3d52282539d0ca7d001e37c405f9a1
|
4
|
+
data.tar.gz: 37d6b834f1857f6119d3ec7969d422f7d4f3fcbeaa1645d8bc0f8a3f10824c04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 180be6b9f59bc10e2bc0e708f4d8ad01d4d3d7a27d95707162ef45c4478ff858d6b6b5a2f598b6c411bf0e4942f02f0a13a642b04746e79fb124c2c6d70e58cb
|
7
|
+
data.tar.gz: db9436660c4fa4e2bdd80af101dbc8723f46781f17eeeecc57e8015a2510d9429bce6f38f93b210c82b2499b3dae787df26c1d0e898280af98174738a29eb984
|
data/CHANGELOG.md
CHANGED
@@ -1,19 +1,39 @@
|
|
1
|
-
## 0.2.
|
1
|
+
## 0.2.8 (2021-01-16)
|
2
|
+
|
3
|
+
- Fixed bug with removing order
|
4
|
+
|
5
|
+
## 0.2.7 (2021-01-16)
|
6
|
+
|
7
|
+
- Fixed error with order
|
8
|
+
|
9
|
+
## 0.2.6 (2020-12-18)
|
10
|
+
|
11
|
+
- Fixed error with certain column types for Active Record 6.1
|
12
|
+
|
13
|
+
## 0.2.5 (2020-09-07)
|
14
|
+
|
15
|
+
- Added warning for non-attribute argument
|
16
|
+
|
17
|
+
## 0.2.4 (2020-03-12)
|
18
|
+
|
19
|
+
- Added `percentile` method
|
20
|
+
|
21
|
+
## 0.2.3 (2019-09-03)
|
2
22
|
|
3
23
|
- Added support for Mongoid
|
4
|
-
- Dropped support for
|
24
|
+
- Dropped support for Active Record 4.2
|
5
25
|
|
6
|
-
## 0.2.2
|
26
|
+
## 0.2.2 (2018-10-29)
|
7
27
|
|
8
28
|
- Added support for MySQL with udf_infusion
|
9
29
|
- Added support for SQL Server and Redshift
|
10
30
|
|
11
|
-
## 0.2.1
|
31
|
+
## 0.2.1 (2018-10-15)
|
12
32
|
|
13
33
|
- Added support for arrays and hashes
|
14
34
|
- Added compatibility with Groupdate 4
|
15
35
|
|
16
|
-
## 0.2.0
|
36
|
+
## 0.2.0 (2018-10-15)
|
17
37
|
|
18
38
|
- Added support for MariaDB 10.3.3+ and SQLite
|
19
39
|
- Use `PERCENTILE_CONT` for 4x performance increase
|
@@ -22,23 +42,23 @@ Breaking
|
|
22
42
|
|
23
43
|
- Dropped support for Postgres < 9.4
|
24
44
|
|
25
|
-
## 0.1.4
|
45
|
+
## 0.1.4 (2016-12-03)
|
26
46
|
|
27
47
|
- Added `drop_function` method
|
28
48
|
|
29
|
-
## 0.1.3
|
49
|
+
## 0.1.3 (2016-03-22)
|
30
50
|
|
31
|
-
- Added support for
|
51
|
+
- Added support for Active Record 5.0
|
32
52
|
|
33
|
-
## 0.1.2
|
53
|
+
## 0.1.2 (2014-12-27)
|
34
54
|
|
35
|
-
- Added support for
|
55
|
+
- Added support for Active Record 4.2
|
36
56
|
|
37
|
-
## 0.1.1
|
57
|
+
## 0.1.1 (2014-08-12)
|
38
58
|
|
39
59
|
- 10x faster median
|
40
60
|
- Added tests
|
41
61
|
|
42
|
-
## 0.1.0
|
62
|
+
## 0.1.0 (2014-03-13)
|
43
63
|
|
44
64
|
- First release
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ActiveMedian
|
2
2
|
|
3
|
-
Median for Active Record, Mongoid, arrays, and hashes
|
3
|
+
Median and percentile for Active Record, Mongoid, arrays, and hashes
|
4
4
|
|
5
5
|
Supports:
|
6
6
|
|
@@ -8,10 +8,11 @@ Supports:
|
|
8
8
|
- MariaDB 10.3.3+
|
9
9
|
- MySQL and SQL (with extensions)
|
10
10
|
- SQL Server 2012+
|
11
|
+
- MongoDB 2.1+
|
11
12
|
|
12
13
|
:fire: Uses native functions for blazing performance
|
13
14
|
|
14
|
-
[![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)
|
15
16
|
|
16
17
|
## Getting Started
|
17
18
|
|
@@ -25,26 +26,22 @@ For MySQL and SQLite, also follow [these instructions](#additional-instructions)
|
|
25
26
|
|
26
27
|
## Models
|
27
28
|
|
28
|
-
|
29
|
-
Item.median(:price)
|
30
|
-
```
|
31
|
-
|
32
|
-
Works with grouping, too.
|
29
|
+
Median
|
33
30
|
|
34
31
|
```ruby
|
35
|
-
|
32
|
+
Item.median(:price)
|
36
33
|
```
|
37
34
|
|
38
|
-
|
35
|
+
Percentile
|
39
36
|
|
40
37
|
```ruby
|
41
|
-
|
38
|
+
Request.percentile(:response_time, 0.95)
|
42
39
|
```
|
43
40
|
|
44
|
-
|
41
|
+
Works with grouping, too
|
45
42
|
|
46
43
|
```ruby
|
47
|
-
|
44
|
+
Order.group(:store_id).median(:total)
|
48
45
|
```
|
49
46
|
|
50
47
|
## User Input
|
@@ -60,6 +57,26 @@ raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
|
|
60
57
|
User.median(column)
|
61
58
|
```
|
62
59
|
|
60
|
+
## Arrays and Hashes
|
61
|
+
|
62
|
+
Median
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
[1, 2, 3].median
|
66
|
+
```
|
67
|
+
|
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
|
+
|
63
80
|
## Additional Instructions
|
64
81
|
|
65
82
|
### MySQL
|
@@ -106,3 +123,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
|
|
106
123
|
- Fix bugs and [submit pull requests](https://github.com/ankane/active_median/pulls)
|
107
124
|
- Write, clarify, or fix documentation
|
108
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
|
+
```
|
@@ -6,9 +6,35 @@ module Enumerable
|
|
6
6
|
elsif !block && respond_to?(:with_scope)
|
7
7
|
with_scope(self) { klass.median(*args) }
|
8
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
|
9
29
|
sorted = map(&block).sort
|
10
|
-
|
11
|
-
|
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
|
12
38
|
end
|
13
39
|
end
|
14
40
|
end
|
data/lib/active_median/model.rb
CHANGED
@@ -1,6 +1,28 @@
|
|
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 =
|
@@ -15,32 +37,45 @@ module ActiveMedian
|
|
15
37
|
over = "PARTITION BY #{group_values.join(", ")}"
|
16
38
|
end
|
17
39
|
|
18
|
-
select(*group_values, "PERCENTILE_CONT(
|
40
|
+
select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
|
19
41
|
else
|
20
42
|
# if mysql gets native function, check (and memoize) version first
|
21
|
-
select(*group_values, "PERCENTILE_CONT(#{column},
|
43
|
+
select(*group_values, "PERCENTILE_CONT(#{column}, #{percentile})")
|
22
44
|
end
|
23
45
|
when /sqlserver/i
|
24
46
|
if group_values.any?
|
25
47
|
over = "PARTITION BY #{group_values.join(", ")}"
|
26
48
|
end
|
27
49
|
|
28
|
-
select(*group_values, "PERCENTILE_CONT(
|
50
|
+
select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
|
29
51
|
when /sqlite/i
|
30
|
-
|
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
|
31
63
|
when /postg/i, /redshift/i # postgis too
|
32
|
-
select(*group_values, "PERCENTILE_CONT(
|
64
|
+
select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column})")
|
33
65
|
else
|
34
66
|
raise "Connection adapter not supported: #{connection.adapter_name}"
|
35
67
|
end
|
36
68
|
|
69
|
+
# same as average
|
70
|
+
relation = relation.unscope(:order).distinct!(false) if group_values.empty?
|
71
|
+
|
37
72
|
result = connection.select_all(relation.to_sql)
|
38
73
|
|
39
74
|
# typecast
|
40
75
|
rows = []
|
41
76
|
columns = result.columns
|
42
77
|
result.rows.each do |untyped_row|
|
43
|
-
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
|
78
|
+
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] })
|
44
79
|
end
|
45
80
|
|
46
81
|
result =
|
@@ -1,21 +1,30 @@
|
|
1
1
|
module ActiveMedian
|
2
2
|
module Mongoid
|
3
|
-
# https://www.compose.com/articles/mongo-metrics-finding-a-happy-median/
|
4
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
|
+
|
5
12
|
relation =
|
6
13
|
all
|
7
|
-
.
|
8
|
-
.
|
9
|
-
.
|
10
|
-
.project(count: {"$
|
11
|
-
.project(
|
12
|
-
.project(
|
13
|
-
.
|
14
|
-
.project(beginValue: {"$arrayElemAt" => ["$values", "$
|
15
|
-
.project(
|
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"]})
|
16
25
|
|
17
26
|
res = collection.aggregate(relation.pipeline).first
|
18
|
-
res ? res["
|
27
|
+
res ? res["result"] : nil
|
19
28
|
end
|
20
29
|
end
|
21
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.8
|
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: 2021-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -108,7 +108,7 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
-
description:
|
111
|
+
description:
|
112
112
|
email: andrew@chartkick.com
|
113
113
|
executables: []
|
114
114
|
extensions: []
|
@@ -126,7 +126,7 @@ homepage: https://github.com/ankane/active_median
|
|
126
126
|
licenses:
|
127
127
|
- MIT
|
128
128
|
metadata: {}
|
129
|
-
post_install_message:
|
129
|
+
post_install_message:
|
130
130
|
rdoc_options: []
|
131
131
|
require_paths:
|
132
132
|
- lib
|
@@ -141,8 +141,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
141
|
- !ruby/object:Gem::Version
|
142
142
|
version: '0'
|
143
143
|
requirements: []
|
144
|
-
rubygems_version: 3.
|
145
|
-
signing_key:
|
144
|
+
rubygems_version: 3.2.3
|
145
|
+
signing_key:
|
146
146
|
specification_version: 4
|
147
|
-
summary: Median for Active Record, Mongoid, arrays, and hashes
|
147
|
+
summary: Median and percentile for Active Record, Mongoid, arrays, and hashes
|
148
148
|
test_files: []
|