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 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: []