active_median 0.2.0 → 0.2.5

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
2
  SHA256:
3
- metadata.gz: 898f80afa3c78ec2137b1c8f2f2dae6e4604ff657f472725f10f9486a82fc32f
4
- data.tar.gz: '080da5da53ea03e6a20f8a5e80d70afdec3e6fa2d1d140e18f680ac361bc077d'
3
+ metadata.gz: cbc48a266db4698647e93b35bdad3e2ed1fc131ec48a2a00e1ac33583f693c4d
4
+ data.tar.gz: 9e49da11f496e63c1fca879b7f6b509cbc80b6551f22743f9f586dfdf6f3d2f3
5
5
  SHA512:
6
- metadata.gz: e60fa9abc00e89da32390c0608a8e297e8ba4a36a869eea44b0ac94c6dcff00a49b71823f1942313d7709489c9af78b52260f8d1c48d739ec842b2aebdc0ebf6
7
- data.tar.gz: a1ee8deb9ba6352732ed008f8bb255c4a7f248bc63beb27d3c74893306af57a0f0d00ae7848c1e6356263a345fbf70151d0b59d633071aa407685117dc6ccb1c
6
+ metadata.gz: 7c37697facc95126f6b11bd29831f0aa685b781242a729c86e938e9f4be0a0b7fbfadbdf8ae246f987f452bb7977de21f2f879860f945f1a7bc9db005f85dc49
7
+ data.tar.gz: d37c90a02b845b8a2cd8e41ed5109da758ca51c9921675f7f63e30e0921e430eed193d6ae02676e3aa08c5eaf97207d29fd8f50540e1be060924022aa818dc03
@@ -1,4 +1,27 @@
1
- ## 0.2.0
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
@@ -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,34 +1,96 @@
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
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
- ## Usage
29
+ Median
12
30
 
13
31
  ```ruby
14
32
  Item.median(:price)
15
33
  ```
16
34
 
17
- Works with grouping, too.
35
+ Percentile
18
36
 
19
37
  ```ruby
20
- Order.group("date_trunc('week', created_at)").median(:total)
38
+ Request.percentile(:response_time, 0.95)
21
39
  ```
22
40
 
23
- ## Installation
41
+ Works with grouping, too
24
42
 
25
- Add this line to your application’s Gemfile:
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
- gem 'active_median'
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
- For SQLite, also follow the instructions below.
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
+ ```
@@ -1,6 +1,6 @@
1
1
  require "active_support"
2
2
 
3
- require "active_median/model"
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
@@ -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,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(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].send(:cast_value, untyped_row[i]) : untyped_row[i] })
28
76
  end
29
77
 
30
- if group_values.any?
31
- Hash[rows.map { |r| [r.size == 2 ? r[0] : r[0..-2], r[-1]] }]
32
- else
33
- rows[0] && rows[0][0]
34
- end
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
@@ -1,3 +1,3 @@
1
1
  module ActiveMedian
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.5"
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.0
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: 2018-10-15 00:00:00.000000000 Z
11
+ date: 2020-09-07 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,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.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
- rubyforge_project:
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 ActiveRecord
147
+ summary: Median and percentile for Active Record, Mongoid, arrays, and hashes
119
148
  test_files: []