active_median 0.2.0 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -6
- data/LICENSE.txt +1 -1
- data/README.md +83 -11
- data/lib/active_median.rb +7 -1
- data/lib/active_median/enumerable.rb +41 -0
- data/lib/active_median/model.rb +63 -10
- data/lib/active_median/mongoid.rb +30 -0
- data/lib/active_median/version.rb +1 -1
- metadata +42 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cbc48a266db4698647e93b35bdad3e2ed1fc131ec48a2a00e1ac33583f693c4d
|
4
|
+
data.tar.gz: 9e49da11f496e63c1fca879b7f6b509cbc80b6551f22743f9f586dfdf6f3d2f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c37697facc95126f6b11bd29831f0aa685b781242a729c86e938e9f4be0a0b7fbfadbdf8ae246f987f452bb7977de21f2f879860f945f1a7bc9db005f85dc49
|
7
|
+
data.tar.gz: d37c90a02b845b8a2cd8e41ed5109da758ca51c9921675f7f63e30e0921e430eed193d6ae02676e3aa08c5eaf97207d29fd8f50540e1be060924022aa818dc03
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,27 @@
|
|
1
|
-
## 0.2.
|
1
|
+
## 0.2.5 (2020-09-07)
|
2
|
+
|
3
|
+
- Added warning for non-attribute argument
|
4
|
+
|
5
|
+
## 0.2.4 (2020-03-12)
|
6
|
+
|
7
|
+
- Added `percentile` method
|
8
|
+
|
9
|
+
## 0.2.3 (2019-09-03)
|
10
|
+
|
11
|
+
- Added support for Mongoid
|
12
|
+
- Dropped support for Rails 4.2
|
13
|
+
|
14
|
+
## 0.2.2 (2018-10-29)
|
15
|
+
|
16
|
+
- Added support for MySQL with udf_infusion
|
17
|
+
- Added support for SQL Server and Redshift
|
18
|
+
|
19
|
+
## 0.2.1 (2018-10-15)
|
20
|
+
|
21
|
+
- Added support for arrays and hashes
|
22
|
+
- Added compatibility with Groupdate 4
|
23
|
+
|
24
|
+
## 0.2.0 (2018-10-15)
|
2
25
|
|
3
26
|
- Added support for MariaDB 10.3.3+ and SQLite
|
4
27
|
- Use `PERCENTILE_CONT` for 4x performance increase
|
@@ -7,23 +30,23 @@ Breaking
|
|
7
30
|
|
8
31
|
- Dropped support for Postgres < 9.4
|
9
32
|
|
10
|
-
## 0.1.4
|
33
|
+
## 0.1.4 (2016-12-03)
|
11
34
|
|
12
35
|
- Added `drop_function` method
|
13
36
|
|
14
|
-
## 0.1.3
|
37
|
+
## 0.1.3 (2016-03-22)
|
15
38
|
|
16
39
|
- Added support for ActiveRecord 5.0
|
17
40
|
|
18
|
-
## 0.1.2
|
41
|
+
## 0.1.2 (2014-12-27)
|
19
42
|
|
20
43
|
- Added support for ActiveRecord 4.2
|
21
44
|
|
22
|
-
## 0.1.1
|
45
|
+
## 0.1.1 (2014-08-12)
|
23
46
|
|
24
47
|
- 10x faster median
|
25
48
|
- Added tests
|
26
49
|
|
27
|
-
## 0.1.0
|
50
|
+
## 0.1.0 (2014-03-13)
|
28
51
|
|
29
52
|
- First release
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,34 +1,96 @@
|
|
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://travis-ci.org/ankane/active_median.svg)](https://travis-ci.org/ankane/active_median)
|
15
|
+
[![Build Status](https://travis-ci.org/ankane/active_median.svg?branch=master)](https://travis-ci.org/ankane/active_median)
|
16
|
+
|
17
|
+
## Getting Started
|
18
|
+
|
19
|
+
Add this line to your application’s Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'active_median'
|
23
|
+
```
|
24
|
+
|
25
|
+
For MySQL and SQLite, also follow [these instructions](#additional-instructions).
|
26
|
+
|
27
|
+
## Models
|
10
28
|
|
11
|
-
|
29
|
+
Median
|
12
30
|
|
13
31
|
```ruby
|
14
32
|
Item.median(:price)
|
15
33
|
```
|
16
34
|
|
17
|
-
|
35
|
+
Percentile
|
18
36
|
|
19
37
|
```ruby
|
20
|
-
|
38
|
+
Request.percentile(:response_time, 0.95)
|
21
39
|
```
|
22
40
|
|
23
|
-
|
41
|
+
Works with grouping, too
|
24
42
|
|
25
|
-
|
43
|
+
```ruby
|
44
|
+
Order.group(:store_id).median(:total)
|
45
|
+
```
|
46
|
+
|
47
|
+
## User Input
|
48
|
+
|
49
|
+
If passing user input as the column, be sure to sanitize it first [like you must](https://rails-sqli.org/) with other aggregate methods like `sum`.
|
26
50
|
|
27
51
|
```ruby
|
28
|
-
|
52
|
+
column = params[:column]
|
53
|
+
|
54
|
+
# check against permitted columns
|
55
|
+
raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
|
56
|
+
|
57
|
+
User.median(column)
|
58
|
+
```
|
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 }
|
29
78
|
```
|
30
79
|
|
31
|
-
|
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
|
+
```
|
32
94
|
|
33
95
|
### SQLite
|
34
96
|
|
@@ -51,7 +113,7 @@ db.enable_load_extension(0)
|
|
51
113
|
|
52
114
|
### 0.2.0
|
53
115
|
|
54
|
-
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.
|
55
117
|
|
56
118
|
## Contributing
|
57
119
|
|
@@ -61,3 +123,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
|
|
61
123
|
- Fix bugs and [submit pull requests](https://github.com/ankane/active_median/pulls)
|
62
124
|
- Write, clarify, or fix documentation
|
63
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,6 @@
|
|
1
1
|
require "active_support"
|
2
2
|
|
3
|
-
require "active_median/
|
3
|
+
require "active_median/enumerable"
|
4
4
|
require "active_median/version"
|
5
5
|
|
6
6
|
module ActiveMedian
|
@@ -14,5 +14,11 @@ module ActiveMedian
|
|
14
14
|
end
|
15
15
|
|
16
16
|
ActiveSupport.on_load(:active_record) do
|
17
|
+
require "active_median/model"
|
17
18
|
extend(ActiveMedian::Model)
|
18
19
|
end
|
20
|
+
|
21
|
+
ActiveSupport.on_load(:mongoid) do
|
22
|
+
require "active_median/mongoid"
|
23
|
+
Mongoid::Document::ClassMethods.include(ActiveMedian::Mongoid)
|
24
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Enumerable
|
2
|
+
unless method_defined?(:median)
|
3
|
+
def median(*args, &block)
|
4
|
+
if !block && respond_to?(:scoping)
|
5
|
+
scoping { @klass.median(*args) }
|
6
|
+
elsif !block && respond_to?(:with_scope)
|
7
|
+
with_scope(self) { klass.median(*args) }
|
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
|
29
|
+
sorted = map(&block).sort
|
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
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
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,16 +71,20 @@ 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].send(:cast_value, untyped_row[i]) : untyped_row[i] })
|
28
76
|
end
|
29
77
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
78
|
+
result =
|
79
|
+
if group_values.any?
|
80
|
+
Hash[rows.map { |r| [r.size == 2 ? r[0] : r[0..-2], r[-1]] }]
|
81
|
+
else
|
82
|
+
rows[0] && rows[0][0]
|
83
|
+
end
|
84
|
+
|
85
|
+
result = Groupdate.process_result(relation, result) if defined?(Groupdate.process_result)
|
86
|
+
|
87
|
+
result
|
35
88
|
end
|
36
89
|
end
|
37
90
|
end
|
@@ -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.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-07 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,34 @@ 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'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: groupdate
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description:
|
84
112
|
email: andrew@chartkick.com
|
85
113
|
executables: []
|
@@ -90,7 +118,9 @@ files:
|
|
90
118
|
- LICENSE.txt
|
91
119
|
- README.md
|
92
120
|
- lib/active_median.rb
|
121
|
+
- lib/active_median/enumerable.rb
|
93
122
|
- lib/active_median/model.rb
|
123
|
+
- lib/active_median/mongoid.rb
|
94
124
|
- lib/active_median/version.rb
|
95
125
|
homepage: https://github.com/ankane/active_median
|
96
126
|
licenses:
|
@@ -104,16 +134,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
134
|
requirements:
|
105
135
|
- - ">="
|
106
136
|
- !ruby/object:Gem::Version
|
107
|
-
version: '2.
|
137
|
+
version: '2.4'
|
108
138
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
139
|
requirements:
|
110
140
|
- - ">="
|
111
141
|
- !ruby/object:Gem::Version
|
112
142
|
version: '0'
|
113
143
|
requirements: []
|
114
|
-
|
115
|
-
rubygems_version: 2.7.7
|
144
|
+
rubygems_version: 3.1.2
|
116
145
|
signing_key:
|
117
146
|
specification_version: 4
|
118
|
-
summary: Median for
|
147
|
+
summary: Median and percentile for Active Record, Mongoid, arrays, and hashes
|
119
148
|
test_files: []
|