comparability 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d7df560ce69d77e22e8c096500a37c2ed83c271c
4
+ data.tar.gz: 959f6c8776ac7c4922246edd4e5d20e68149c4c8
5
+ SHA512:
6
+ metadata.gz: 66d97494c122f12583f01d78c949a063b28a00d922e4a8a5113edc354e84fe0efbfc248eaf55e956dca16f4319e7c3a1fc6e09b9e54130cbfbc1d8e589ea2bd7
7
+ data.tar.gz: b3805cdb0f7d3c6a949187b581508fdbbd5965466d63c138ef7d18f8b69660fe0cf17353c1ce21ef378c53efd9ebcdda5ba38e69db7f4edd411951b74a409986
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ippei Ukai
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,126 @@
1
+ # Comparability
2
+
3
+ Provides Comparator and declarative definition of comparison operator.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'comparability'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install comparability
18
+
19
+ ## Usage
20
+
21
+ ### ComparableBy (Declarative definition of comparison operator)
22
+
23
+ You can declaratively define your class's comparison with ``comparable_by`` method.
24
+ To use, you need to extend your class with ``ComparableBy`` module.
25
+
26
+ require 'comparability'
27
+
28
+ class MyClass < Struct.new(:a, :b, :c, :d)
29
+ extend ComparableBy
30
+
31
+ attr_reader :a, :b, :c, :d, :e
32
+
33
+ comparable_by :a, # order of a
34
+ [:b, reverse: true], # reverse order of b
35
+ [:c, nil: :first], # order of c, ordered first if c is nil
36
+ [:d, reverse: true, nil: :last] # reverse order of d, ordered last if d is nil
37
+ ->(_){ _.e.upcase }, # order of e, case-insensitive
38
+ end
39
+
40
+ Call of ``comparable_by`` defines ``<=>`` instance method, and includes ``Comparable`` module.
41
+ Each parameter to ``comparable_by`` should be either an instance of ``Comparator`` or arguments to ``Comparator.create``
42
+ in order of precedence.
43
+
44
+ ### Comparator
45
+
46
+ Some pre-defined comparisons are accessible with special methods.
47
+ Other variations are created with ``Comparator.create``.
48
+
49
+ #### natural order
50
+
51
+ The most simple ``Comparator`` which just evokes the comparison (spaceship) operator.
52
+
53
+ Comparator.natural_order # essentially compare(a,b) is defined as a <=> b
54
+ Comparator.natural_order(reverse: true) # b <=> a instead
55
+
56
+ #### nil priority
57
+
58
+ This special ``Comparator`` orders only by whether object is nil or not.
59
+
60
+ Comparator.prioritize_nil(:first) # nil is smaller
61
+ Comparator.prioritize_nil(:last) # nil is bigger
62
+
63
+ #### by attribute value
64
+
65
+ If you specify a symbol to ``Comparator.create``,
66
+ the comparator will compare objects by the results of the specified method.
67
+
68
+ Comparator.create(:size) # compares by size (prioritise smaller)
69
+ Comparator.create(:size, reverse: true) # compares by size (prioritise bigger)
70
+
71
+ You can use ``:nil`` option to specify the priority of nil value.
72
+
73
+ Comparator.create(:priority, nil: :last) # compares by priority (prioritise smaller, last if nil)
74
+ Comparator.create(:priority, reverse: true, nil: :last) # compares by priority (prioritise bigger, last if nil)
75
+
76
+ The attribute method can be public or protected, but not private.
77
+
78
+ #### by value extractor
79
+
80
+ If you specify a proc with arity 1 to ``Comparator.create``,
81
+ the comparator will compare objects by the results of the specified proc yielding each object.
82
+
83
+ You can also pass the comparison as block.
84
+
85
+ Comparator.create { |str| str.length } # compares by length (prioritise shorter)
86
+ Comparator.create { |str| -str.length } # compares by length (prioritise longer)
87
+
88
+ #### with proc
89
+
90
+ If you specify a proc with arity 2 to ``Comparator.create``,
91
+ it creates a comparator defined by the specified proc.
92
+
93
+ You can also pass the comparison as block.
94
+
95
+ Comparator.create do |obj1, obj2|
96
+ # ... your comparison here ...
97
+ end
98
+
99
+ #### chaining
100
+
101
+ You can chain comparators with ``Comparator.chain`` or its alias ``Comparator.[]``.
102
+
103
+ Comparator[comparator1, comparator2] # compares with comparator1, then tie-breaks with comparator2
104
+
105
+ #### reversing
106
+
107
+ You can reverse existing comparators with ``Comparator.reverse``.
108
+
109
+ Comparator.reverse(comparator) # reverses the order
110
+
111
+ #### ``Enumerable#sort`` usage
112
+
113
+ ``Comparator#to_proc`` returns a ``Proc`` compatible with ``Enumerable#sort``.
114
+ You can use the ampersand shorthand to convert a comparator and pass as block.
115
+
116
+ comparator = Comparator[Comparator.prioritize_nil(:last),
117
+ Comparator.natural_order(reverse: true)]
118
+ [nil,1,2,3,4].sort(&comparator) # => [4, 3, 2, 1, nil]
119
+
120
+ ## Contributing
121
+
122
+ 1. Fork it ( http://github.com/ippeiukai/comparability/fork )
123
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
124
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
125
+ 4. Push to the branch (`git push origin my-new-feature`)
126
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'comparability/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'comparability'
8
+ spec.version = Comparability::VERSION
9
+ spec.authors = ['Ippei Ukai']
10
+ spec.email = ['ippei@users.sourceforge.net']
11
+ spec.summary = 'Provides Comparator and declarative definition of comparison operator.'
12
+ spec.homepage = 'http://github.com/ippeiukai/comparability'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x00")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.5'
21
+ spec.add_development_dependency 'rake'
22
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'comparability/version'
2
+
3
+ require_relative 'comparability/comparator'
4
+ Comparator = Comparability::Comparator
5
+
6
+ require_relative 'comparability/comparable_by'
7
+ ComparableBy = Comparability::ComparableBy
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+
3
+ module Comparability
4
+ module ComparableBy
5
+
6
+ private
7
+
8
+ # @!visibility public
9
+ # Declares the comparison for this class.
10
+ # This defines <=> instance method and includes Comparable module.
11
+ def comparable_by(*args)
12
+ raise ArgumentError if args.empty?
13
+ comparator = Comparator.chain(*args)
14
+ const_set(:COMPARATOR, comparator)
15
+ define_method(:<=>) { |other| self.class::COMPARATOR.compare(self, other) }
16
+ include Comparable
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+
3
+ module Comparability
4
+ class Comparator
5
+ # @!parse extend Comparability::Comparators::FactoryMethods
6
+
7
+ def compare(me, other)
8
+ me <=> other
9
+ end
10
+
11
+ def to_proc
12
+ @_to_proc ||= method(:compare).to_proc
13
+ end
14
+
15
+ end
16
+ end
17
+
18
+ # require files in ./comparators
19
+ File.dirname(File.expand_path(__FILE__)).tap do |this_dirname|
20
+ Dir[File.join(this_dirname, 'comparators', '**', '*.rb')].each do |file|
21
+ name = file.gsub(%r|^#{Regexp.escape(this_dirname)}/(.*)#{Regexp.escape('.rb')}|, '\1')
22
+ require_relative name
23
+ end
24
+ end
25
+
26
+ Comparability::Comparator.extend(Comparability::Comparators::FactoryMethods)
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+
3
+ require_relative 'value_comparator'
4
+
5
+ module Comparability
6
+ module Comparators
7
+ class AttributeValueComparator < ValueComparator
8
+
9
+ def initialize(attribute)
10
+ attribute = attribute.to_s.freeze unless attribute.is_a?(Symbol)
11
+ @attribute = attribute
12
+ end
13
+
14
+ attr_reader :attribute
15
+
16
+ private
17
+
18
+ def extract_value(me)
19
+ me.send(attribute)
20
+ end
21
+
22
+ def extract_other_value(me, _other_)
23
+ if instance_evaluable_attribute?
24
+ # me can access the protected attribute of the other
25
+ me.instance_eval("_other_.#{attribute}()")
26
+ else
27
+ _other_.public_send(attribute)
28
+ end
29
+ end
30
+
31
+ INSTANCE_EVALUABLE_METHOD_NAME = /\A([_a-zA-Z][_a-zA-Z0-9]*[!?]?)|([!~+-]@)\Z/
32
+ def instance_evaluable_attribute?
33
+ INSTANCE_EVALUABLE_METHOD_NAME =~ attribute
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+
3
+ require 'forwardable'
4
+
5
+ module Comparability
6
+ module Comparators
7
+ class ComparatorChain < Comparator
8
+ extend Forwardable
9
+ include Enumerable
10
+
11
+ def initialize(comparators)
12
+ raise ArgumentError unless comparators.size > 0
13
+ raise ArgumentError unless comparators.all? { |c| c.is_a?(Comparator) }
14
+ @comparators = comparators.freeze
15
+ end
16
+
17
+ attr_reader :comparators
18
+ delegate :each => :comparators
19
+
20
+ def compare(me, other)
21
+ inject(0) do |zero, comparator|
22
+ result = comparator.compare(me, other)
23
+ if result.nil?
24
+ # the comparator failed
25
+ return nil
26
+ elsif result.nonzero?
27
+ # we have the conclusion
28
+ return result
29
+ else
30
+ # a tie
31
+ next 0
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,103 @@
1
+ # coding: utf-8
2
+
3
+
4
+ module Comparability
5
+ module Comparators
6
+ module FactoryMethods
7
+
8
+ def create(comparison = nil, options = {}, &block)
9
+ comparison ||= block
10
+ comparator =
11
+ case comparison
12
+ when Symbol
13
+ create_from_symbol(comparison)
14
+ when Proc
15
+ create_from_proc(comparison)
16
+ when ->(_) { _.respond_to?(:to_proc) }
17
+ create_from_proc(comparison.to_proc)
18
+ else
19
+ raise ArgumentError, 'invalid comparison'
20
+ end
21
+ apply_options(comparator, options)
22
+ end
23
+
24
+ # @param comparisons [Array]
25
+ # each comparison should be either Comparison instance or argument(s) accepted by #create
26
+ def chain(*comparisons)
27
+ comparators = comparisons.map do |comparison_or_comparator|
28
+ case comparison_or_comparator
29
+ when Comparator
30
+ comparison_or_comparator
31
+ else
32
+ create(*comparison_or_comparator)
33
+ end
34
+ end
35
+ ComparatorChain.new(comparators)
36
+ end
37
+ alias_method :[], :chain
38
+
39
+ # @param priority [Symbol] :first or :last
40
+ def prioritize_nil(priority)
41
+ NilComparator.with_nil_priority(priority)
42
+ end
43
+
44
+ # @option options [true] :reverse
45
+ def natural_order(options = {})
46
+ @_natural_order ||= Comparator.new
47
+ if options[:reverse]
48
+ @_reverse_natural_order ||= reverse(@_natural_order)
49
+ else
50
+ @_natural_order
51
+ end
52
+ end
53
+
54
+ # @param comparator [Comparator]
55
+ def reverse(comparator)
56
+ ReverseWrapperComparator.wrap(comparator)
57
+ end
58
+
59
+ private
60
+
61
+ def create_from_symbol(attr)
62
+ AttributeValueComparator.new(attr)
63
+ end
64
+
65
+ def create_from_proc(proc)
66
+ case proc.arity
67
+ when 1
68
+ ValueExtractorComparator.new(proc)
69
+ when 2
70
+ ProcComparator.new(proc)
71
+ else
72
+ raise ArgumentError, 'invalid comparison'
73
+ end
74
+ end
75
+
76
+ def apply_options(comparator, options)
77
+ nil_value_priority = options.fetch(:nil, nil)
78
+ reversing = options.fetch(:reverse, false)
79
+
80
+ if nil_value_priority
81
+ raise ArgumentError unless comparator.respond_to?(:nil_value_priority=)
82
+ if reversing
83
+ # value comparison should be reversed, but not the nil priority
84
+ case nil_value_priority
85
+ when :first
86
+ nil_value_priority = :last
87
+ when :last
88
+ nil_value_priority = :first
89
+ end
90
+ end
91
+ comparator.nil_value_priority = nil_value_priority
92
+ end
93
+
94
+ if reversing
95
+ comparator = reverse(comparator)
96
+ end
97
+
98
+ comparator
99
+ end
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+
3
+ require 'singleton'
4
+
5
+ module Comparability
6
+ module Comparators
7
+ module NilComparator
8
+
9
+ class << self
10
+
11
+ def with_nil_priority(priority)
12
+ raise ArgumentError unless %[first last].include?(priority.to_s)
13
+ public_send("nil_#{priority}")
14
+ end
15
+
16
+ def nil_first
17
+ @_nil_first ||= ReverseWrapperComparator.wrap(NilLastComparator.instance)
18
+ end
19
+
20
+ def nil_last
21
+ NilLastComparator.instance
22
+ end
23
+
24
+ end
25
+
26
+ class NilLastComparator < Comparator
27
+ include Singleton
28
+
29
+ def compare(me, other)
30
+ if me.nil? && other.nil?
31
+ 0
32
+ elsif me.nil?
33
+ 1
34
+ elsif other.nil?
35
+ -1
36
+ else
37
+ 0
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ # coding: utf-8
2
+
3
+ module Comparability
4
+ module Comparators
5
+ class ProcComparator < Comparator
6
+
7
+ def initialize(comparison)
8
+ raise ArgumentError unless comparison.is_a?(Proc) && comparison.arity == 2
9
+ self.singleton_class.send(:define_method, :compare, &comparison)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+
3
+ require_relative 'wrapper_comparator'
4
+
5
+ module Comparability
6
+ module Comparators
7
+ class ReverseWrapperComparator < WrapperComparator
8
+
9
+ def compare(me, other)
10
+ reverse(wrapped_compare(me, other))
11
+ end
12
+
13
+ private
14
+
15
+ def reverse(comparison_result)
16
+ if comparison_result.nil? || comparison_result.zero?
17
+ comparison_result
18
+ else
19
+ -comparison_result
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+
3
+ require_relative 'nil_comparator'
4
+
5
+ module Comparability
6
+ module Comparators
7
+ class ValueComparator < Comparator
8
+
9
+ def compare(me, other)
10
+ me_value = extract_value(me)
11
+ other_value = extract_other_value(me, other)
12
+
13
+ if nil_value_priority
14
+ nil_result = NilComparator.with_nil_priority(nil_value_priority).compare(me_value, other_value)
15
+ return nil_result if nil_result && nil_result.nonzero?
16
+ end
17
+
18
+ me_value <=> other_value
19
+ end
20
+
21
+ def nil_value_priority=(priority)
22
+ raise ArgumentError unless [nil, :first, :last].include?(priority)
23
+ @nil_value_priority = priority
24
+ end
25
+
26
+ attr_reader :nil_value_priority
27
+
28
+ private
29
+
30
+ def extract_value(me)
31
+ raise 'abstract'
32
+ end
33
+
34
+ def extract_other_value(me, obj)
35
+ extract_value(obj)
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+
3
+ require_relative 'value_comparator'
4
+
5
+ module Comparability
6
+ module Comparators
7
+ class ValueExtractorComparator < ValueComparator
8
+
9
+ def initialize(value_extractor)
10
+ raise ArgumentError unless value_extractor.is_a?(Proc) && value_extractor.arity == 1
11
+ @value_extractor = value_extractor
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :value_extractor
17
+
18
+ def extract_value(obj)
19
+ value_extractor.(obj)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+
3
+ module Comparability
4
+ module Comparators
5
+ class WrapperComparator < Comparator
6
+
7
+ class << self
8
+ alias_method :wrap, :new
9
+ end
10
+
11
+ def initialize(comparator)
12
+ raise ArgumentError unless comparator.is_a?(Comparator)
13
+ @wrapped = comparator
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :wrapped
19
+
20
+ def wrapped_compare(me, other)
21
+ wrapped.compare(me, other)
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Comparability
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: comparability
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ippei Ukai
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-19 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.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
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
+ description:
42
+ email:
43
+ - ippei@users.sourceforge.net
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - comparability.gemspec
54
+ - lib/comparability.rb
55
+ - lib/comparability/comparable_by.rb
56
+ - lib/comparability/comparator.rb
57
+ - lib/comparability/comparators/attribute_value_comparator.rb
58
+ - lib/comparability/comparators/comparator_chain.rb
59
+ - lib/comparability/comparators/factory_methods.rb
60
+ - lib/comparability/comparators/nil_comparator.rb
61
+ - lib/comparability/comparators/proc_comparator.rb
62
+ - lib/comparability/comparators/reverse_wrapper_comparator.rb
63
+ - lib/comparability/comparators/value_comparator.rb
64
+ - lib/comparability/comparators/value_extractor_comparator.rb
65
+ - lib/comparability/comparators/wrapper_comparator.rb
66
+ - lib/comparability/version.rb
67
+ homepage: http://github.com/ippeiukai/comparability
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 2.2.2
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Provides Comparator and declarative definition of comparison operator.
91
+ test_files: []