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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47d759f5155e0a9687eeb845cf25a65daae5fe2a475bed1dcc071b742413b8c4
4
- data.tar.gz: 9eb8b10f9ff8526adcea04beb4fa68fb11e61b544f11413925280c5d37f10e2b
3
+ metadata.gz: 9f779fe5268fc1347cd94c47fc55a3322a3d52282539d0ca7d001e37c405f9a1
4
+ data.tar.gz: 37d6b834f1857f6119d3ec7969d422f7d4f3fcbeaa1645d8bc0f8a3f10824c04
5
5
  SHA512:
6
- metadata.gz: af49d340ff2b1298d1706381a7879ac3666e9077a56607c2d8809b132f93eef3eb8beb9996aa7599157a3d900ad3951e606413cb936f23d045115d8baead0121
7
- data.tar.gz: ed82a1e318b79372aace81c56d2ca9bd9c59f373be5795f2e595fc8a60c7a7bdd9f9191e5a46069c4ea7405347e3057abb3c1ad6ae794a252cdce6de7f5411b6
6
+ metadata.gz: 180be6b9f59bc10e2bc0e708f4d8ad01d4d3d7a27d95707162ef45c4478ff858d6b6b5a2f598b6c411bf0e4942f02f0a13a642b04746e79fb124c2c6d70e58cb
7
+ data.tar.gz: db9436660c4fa4e2bdd80af101dbc8723f46781f17eeeecc57e8015a2510d9429bce6f38f93b210c82b2499b3dae787df26c1d0e898280af98174738a29eb984
@@ -1,19 +1,39 @@
1
- ## 0.2.3
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 Rails 4.2
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 ActiveRecord 5.0
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 ActiveRecord 4.2
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2019 Andrew Kane
1
+ Copyright (c) 2013-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
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://travis-ci.org/ankane/active_median.svg)](https://travis-ci.org/ankane/active_median)
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
- ```ruby
29
- Item.median(:price)
30
- ```
31
-
32
- Works with grouping, too.
29
+ Median
33
30
 
34
31
  ```ruby
35
- Order.group(:store_id).median(:total)
32
+ Item.median(:price)
36
33
  ```
37
34
 
38
- ## Arrays and Hashes
35
+ Percentile
39
36
 
40
37
  ```ruby
41
- [1, 2, 3].median
38
+ Request.percentile(:response_time, 0.95)
42
39
  ```
43
40
 
44
- You can also pass a block.
41
+ Works with grouping, too
45
42
 
46
43
  ```ruby
47
- {a: 1, b: 2, c: 3}.median { |k, v| v }
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
- len = sorted.length
11
- (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
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
@@ -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(0.50) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
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}, 0.50)")
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(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)
29
51
  when /sqlite/i
30
- 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
31
63
  when /postg/i, /redshift/i # postgis too
32
- select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column})")
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
- .group(_id: nil, count: {"$sum" => 1}, values: {"$push" => "$#{column}"})
8
- .unwind("$values")
9
- .asc(:values)
10
- .project(count: {"$subtract" => ["$count", 1]}, values: 1)
11
- .project(count: 1, values: 1, midpoint: {"$divide" => ["$count", 2]})
12
- .project(count: 1, values: 1, midpoint: 1, high: {"$ceil" => "$midpoint"}, low: {"$floor" => "$midpoint"})
13
- .group(_id: nil, values: {"$push" => "$values"}, high: {"$avg" => "$high"}, low: {"$avg" => "$low"})
14
- .project(beginValue: {"$arrayElemAt" => ["$values", "$high"]}, endValue: {"$arrayElemAt" => ["$values", "$low"]})
15
- .project(median: {"$avg" => ["$beginValue", "$endValue"]})
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["median"] : nil
27
+ res ? res["result"] : nil
19
28
  end
20
29
  end
21
30
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveMedian
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.8"
3
3
  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.3
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: 2019-09-04 00:00:00.000000000 Z
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.0.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: []