convenient_grouper 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OTlmMjVmYzk2YmQwYTk5NTc3NTk4YzM5NWM4NDZmMmY3YjcxZjI4MA==
5
+ data.tar.gz: !binary |-
6
+ MmYxYjAxOWE0MzM4ODhiMTIwYTJlM2M0MGEyN2JmNjE3ZTQzMTZjZA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NmE5MDBhYjRjYTY5ZjYxZGY5YTU0N2Y2OTJlMGM1NjcwMWU1OTRjYWM5NmQ4
10
+ Y2Y5NjEwZjk3OThmNWQ3Y2FlYWQ1YTYxNzhlMjQzOWZkY2E0MGUxNzliYWM0
11
+ OTE0MDZkYzRjZmQ4ODNiYjRhNDAxN2RiZTUxOTcxMjliNzkxMjU=
12
+ data.tar.gz: !binary |-
13
+ Y2E1OWJkNThkNzNlM2FkODU2MjAxNmE2YWNjMzI3ZTIxYzVhNGQwYjBhNzA5
14
+ ZjYyN2Y2MzI1ZjI0NTVhZjIwNjc0MzdiMDg2MjkzNTY1NmVlNzI5MTZlOTFh
15
+ Mzg2MjI2MzMwZmE2NTJiMzdlYTEzNjc4ZmNmMmFjMzNkMjY4Njc=
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.swp
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ convenient_grouper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.2.0
6
+ env: RAKE_ENV=test RAILS_ENV=test
7
+ before_install:
8
+ - 'gem install bundler'
9
+ script:
10
+ - 'bundle exec rspec'
11
+ notifications:
12
+ email:
13
+ recipients:
14
+ - humzashah+travis@gmail.com
15
+ on_success: never
16
+ on_failure: always
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # ConvenientGrouper
2
+
3
+ This gem simplifies grouping records. The primary intention is to make group-aggregation-methods easier.
4
+
5
+ To use, add the following to your Gemfile.
6
+
7
+ ```ruby
8
+ gem 'convenient_grouper'
9
+ ```
10
+
11
+ After `bundle install`, you can provide the grouping details as a hash with the column as the hash's key.
12
+
13
+ ### Usage:
14
+
15
+ ```ruby
16
+ ModelName.group(grouping_hash, options)
17
+ ```
18
+
19
+ First argument to method is a grouping hash:
20
+
21
+ ```ruby
22
+ grouping_hash = {
23
+ column_name: {
24
+ group_name_one: a_range_or_array, # group if column value within range/array
25
+ group_name_two: a_specific_value, # group if column value equals
26
+
27
+ # comparison operators: !=, <, <=, >, >=
28
+ # work best for values whose to_s does not yield spaces
29
+ group_name_four: "!= #{some_value_one}",
30
+ group_name_five: "> #{some_value_two}",
31
+ group_name_six: "<= #{some_value_three}",
32
+
33
+ # default group
34
+ nil => some_string # default value is 'others'
35
+ }
36
+ }
37
+ ```
38
+
39
+ Second argument is an optional hash:
40
+
41
+ ```ruby
42
+ options = {
43
+ restrict: boolean # restrict query to grouped conditions. defaults to false.
44
+ }
45
+ ```
46
+
47
+ If you pass only one non-hash argument, you will be using `ActiveRecord`'s own `group` method.
48
+
49
+ ### Example 1
50
+
51
+ Getting profits for different dates and date-ranges from a `Sale` model:
52
+
53
+ ```ruby
54
+ Sale.group({
55
+ date_of_sale: {
56
+ Jan: Date.new(2015, 1)..Date.new(2015, 1, 31),
57
+ date: Date.new(2014, 4, 5),
58
+ dates: [Date.new(2014, 1, 2), Date.new(2014, 2, 1)],
59
+ unspecified: nil
60
+ }
61
+ }).sum(:profit)
62
+ #=> {"Jan" => 1550, "date" => 50, "dates" => 100, "unspecified" => 25, "others" => 50000}
63
+ ```
64
+
65
+ ### Example 2
66
+
67
+ Counting employees in various age-groups:
68
+
69
+ ```ruby
70
+ Employee.group({
71
+ age: {
72
+ young_adults: 18..25,
73
+ adults: 25..35,
74
+ seniors: ">= 60",
75
+ nil => 'mature_adults'
76
+ }
77
+ }).count
78
+ #=> {"young_adults" => 13, "adults" => 15, "seniors" => 4, "mature_adults" => 5}
79
+ ```
80
+
81
+ ### Example 3
82
+
83
+ Grouping events that are occurring after a particular date:
84
+
85
+ ```ruby
86
+ date = Date.new(2015, 6, 22)
87
+ Event.group(date: {
88
+ not_on_date: ">= #{date}"
89
+ })
90
+ ```
91
+
92
+ ### Example 4
93
+
94
+ Restricting your query to the conditions specified in your `group` clause:
95
+
96
+ ```ruby
97
+ range = 13..19
98
+ Relative.group({
99
+ age: {teens: range}
100
+ }, restrict: true) # == Employee.where(age: range).group(hash)
101
+ ```
102
+
103
+ ### Heads-Up
104
+
105
+ A heads-up for specifying the extra options such as `restrict`:
106
+
107
+ Ruby will interpret the following as a single hash-argument:
108
+
109
+ ```ruby
110
+ Relative.group(age: {teens: 13..19}, restrict: true) # wrong
111
+ ```
112
+
113
+ Therefore you will need appropriate curly braces:
114
+
115
+ ```ruby
116
+ Relative.group({age: {teens: 13..19}}, {restrict: true}) # right
117
+ ```
118
+
119
+ ### Bug Reports
120
+
121
+ Things seemingly work well for the most usual use-cases. Nonetheless, bug-reports are welcomed.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "convenient_grouper"
5
+
6
+ require "irb"
7
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'convenient_grouper/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "convenient_grouper"
8
+ spec.version = ConvenientGrouper::VERSION
9
+ spec.authors = ["Syed Humza Shah"]
10
+ spec.email = ["humzashah+github@gmail.com"]
11
+
12
+ spec.summary = "Use Ruby hashes to group database table rows through ActiveRecord."
13
+ spec.homepage = "https://github.com/humzashah/convenient_grouper"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.required_ruby_version = '>= 1.9.3'
21
+
22
+ spec.add_development_dependency "bundler", ">= 1.8"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "activerecord", ">= 4.0"
25
+ spec.add_development_dependency "rspec"
26
+ end
@@ -0,0 +1,45 @@
1
+ require "active_record"
2
+ require "convenient_grouper/hash_converter"
3
+ require "convenient_grouper/version"
4
+
5
+ module ConvenientGrouper
6
+ module_function
7
+
8
+ def preliminaries(hash_arg, opts)
9
+ hc = get_hash_converter(hash_arg, opts)
10
+
11
+ {
12
+ groups: get_groups(hc, hash_arg),
13
+ restrictions: get_restrictions(hc)
14
+ }
15
+ end
16
+
17
+ private
18
+ module_function
19
+
20
+ def get_hash_converter(hash_arg, opts)
21
+ return unless hash_arg.is_a?(Hash)
22
+ ConvenientGrouper::HashConverter.new(hash_arg, opts)
23
+ end
24
+
25
+ def get_groups(hc, hash_arg)
26
+ hc.try(:groups) || hash_arg
27
+ end
28
+
29
+ def get_restrictions(hc)
30
+ hc.try(:restrictions) || ""
31
+ end
32
+ end
33
+
34
+ ActiveRecord::Relation.class_eval do
35
+ define_method :group do |hash_arg, opts={}|
36
+ if hash_arg.is_a?(Hash)
37
+ hash = ConvenientGrouper.preliminaries(hash_arg, opts)
38
+ groups = hash[:groups]
39
+ restrictions = hash[:restrictions]
40
+ super(groups).where(restrictions)
41
+ else
42
+ super(hash_arg)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,4 @@
1
+ module ConvenientGrouper
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,144 @@
1
+ require_relative "error"
2
+ require_relative "regex"
3
+
4
+ module ConvenientGrouper
5
+ class HashConverter
6
+ attr_reader :groups, :restrictions
7
+
8
+ module Default
9
+ GROUP = 'others'
10
+
11
+ OPTIONS = {
12
+ restrict: false
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def initialize(hash_arg, options=Default::OPTIONS)
19
+ @hash = hash_arg
20
+ validate_hash
21
+
22
+ @options = options
23
+ validate_options
24
+
25
+ @restrict = (@options[:restrict] == true)
26
+
27
+ create_groups
28
+ create_restrictions
29
+ end
30
+
31
+ def validate_hash
32
+ raise_error("Incompatible hash: #{@hash}.") unless matches_conditions?
33
+ end
34
+
35
+ def validate_options
36
+ valid_keys = Default::OPTIONS.keys
37
+ invalid_keys = @options.keys - valid_keys
38
+ return if valid = invalid_keys.empty?
39
+
40
+ valid_keys_str = valid_keys.join(', ')
41
+ invalid_keys_str = invalid_keys.join(', ')
42
+ raise_error "You provided invalid options: #{invalid_keys_str}. Supported options include: #{valid_keys_str}."
43
+ end
44
+
45
+ def matches_conditions?
46
+ @hash.is_a?(Hash) &&
47
+ (@hash.each_key.count == 1) &&
48
+ @hash.each_value.all? { |v| v.is_a?(Hash) }
49
+ end
50
+
51
+ def create_groups
52
+ @groups = "CASE #{[*cases, default_group].compact.join(' ')} END"
53
+ end
54
+
55
+ def create_restrictions
56
+ @restrictions =
57
+ if @restrict
58
+ parse_hash do |column, _, value|
59
+ column_value_matcher(column, value)
60
+ end.join(' OR ')
61
+ end || ""
62
+ end
63
+
64
+ def cases
65
+ parse_hash do |column, group, value|
66
+ "WHEN #{column_value_matcher(column, value)} THEN '#{group}'"
67
+ end
68
+ end
69
+
70
+ def parse_hash
71
+ @hash.each_with_object([]) do |(column, values_hash), array|
72
+ values_hash.each do |group, value|
73
+ array << yield(column, group, value) if group
74
+ end
75
+ end
76
+ end
77
+
78
+ def column_value_matcher(column, value)
79
+ "(#{column} #{lookup_str(value)})"
80
+ end
81
+
82
+ def default_group
83
+ hash = @hash.values.first
84
+ group = hash[nil] || Default::GROUP
85
+ "ELSE '#{group}'"
86
+ end
87
+
88
+ def lookup_str(value)
89
+ case value
90
+ when Range
91
+ range_str(value)
92
+ when Array
93
+ array_str(value)
94
+ when lambda { |x| Regex.matches?(x) }
95
+ value
96
+ when Numeric, String
97
+ value_str(value)
98
+ when NilClass
99
+ 'IS NULL'
100
+ else
101
+ raise_error("Unsupported type #{value.class.name} for #{value}.")
102
+ end
103
+ end
104
+
105
+ def range_str(range)
106
+ validate_range(range)
107
+
108
+ min = insert_val(range.min)
109
+ max = insert_val(range.max)
110
+
111
+ "BETWEEN #{min} AND #{max}"
112
+ end
113
+
114
+ def validate_range(range)
115
+ min = range.min
116
+ max = range.max
117
+
118
+ valid = min && max && (min < max)
119
+ raise_error("Improper range: #{range}.") unless valid
120
+ end
121
+
122
+ def array_str(array)
123
+ mapped_str = array.map { |v| insert_val(v) }.join(', ')
124
+ "IN (#{mapped_str})"
125
+ end
126
+
127
+ def value_str(value)
128
+ "= #{insert_val(value)}"
129
+ end
130
+
131
+ def insert_val(value)
132
+ if value.is_a?(Numeric)
133
+ value
134
+ else
135
+ "'#{value.to_s}'"
136
+ end
137
+ end
138
+
139
+ def raise_error(msg)
140
+ raise Error.new(msg)
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,20 @@
1
+ module ConvenientGrouper
2
+ module Regex
3
+
4
+ module Expression
5
+ COMPARISON = /^[<, >]={0,1}\s.+$/ # e.g. '> 1' and '<= 4'
6
+ INEQUALITY = /!=.+/ # e.g. "!= 4" and "!= 'one'"
7
+ NOT_NULL = /^IS NOT NULL$/i
8
+ end
9
+
10
+ ALL_EXPRESSIONS = Expression.constants.map { |symbol| Expression.const_get(symbol) }
11
+
12
+ module_function
13
+ def matches?(value)
14
+ value.is_a?(String) &&
15
+ ALL_EXPRESSIONS.any? do |expression|
16
+ expression.match(value.strip)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module ConvenientGrouper
2
+ VERSION = "0.1.6"
3
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: convenient_grouper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.6
5
+ platform: ruby
6
+ authors:
7
+ - Syed Humza Shah
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - humzashah+github@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .ruby-gemset
78
+ - .ruby-version
79
+ - .travis.yml
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - bin/console
85
+ - bin/setup
86
+ - convenient_grouper.gemspec
87
+ - lib/convenient_grouper.rb
88
+ - lib/convenient_grouper/error.rb
89
+ - lib/convenient_grouper/hash_converter.rb
90
+ - lib/convenient_grouper/regex.rb
91
+ - lib/convenient_grouper/version.rb
92
+ homepage: https://github.com/humzashah/convenient_grouper
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: 1.9.3
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.4.3
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Use Ruby hashes to group database table rows through ActiveRecord.
116
+ test_files: []