jinx 2.1.1
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.
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +27 -0
- data/History.md +6 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +41 -0
- data/examples/family/README.md +10 -0
- data/examples/family/ext/build.xml +35 -0
- data/examples/family/ext/src/family/Address.java +68 -0
- data/examples/family/ext/src/family/Child.java +24 -0
- data/examples/family/ext/src/family/DomainObject.java +26 -0
- data/examples/family/ext/src/family/Household.java +36 -0
- data/examples/family/ext/src/family/Parent.java +48 -0
- data/examples/family/ext/src/family/Person.java +42 -0
- data/examples/family/lib/family.rb +15 -0
- data/examples/family/lib/family/address.rb +6 -0
- data/examples/family/lib/family/domain_object.rb +6 -0
- data/examples/family/lib/family/household.rb +6 -0
- data/examples/family/lib/family/parent.rb +16 -0
- data/examples/family/lib/family/person.rb +6 -0
- data/examples/model/README.md +25 -0
- data/examples/model/ext/build.xml +35 -0
- data/examples/model/ext/src/domain/Child.java +192 -0
- data/examples/model/ext/src/domain/Dependent.java +29 -0
- data/examples/model/ext/src/domain/DomainObject.java +26 -0
- data/examples/model/ext/src/domain/Independent.java +83 -0
- data/examples/model/ext/src/domain/Parent.java +129 -0
- data/examples/model/ext/src/domain/Person.java +14 -0
- data/examples/model/lib/model.rb +13 -0
- data/examples/model/lib/model/child.rb +13 -0
- data/examples/model/lib/model/domain_object.rb +6 -0
- data/examples/model/lib/model/independent.rb +11 -0
- data/examples/model/lib/model/parent.rb +17 -0
- data/jinx.gemspec +22 -0
- data/lib/jinx.rb +3 -0
- data/lib/jinx/active_support/README.txt +2 -0
- data/lib/jinx/active_support/core_ext/string.rb +7 -0
- data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/jinx/active_support/inflections.rb +55 -0
- data/lib/jinx/active_support/inflector.rb +398 -0
- data/lib/jinx/cli/application.rb +36 -0
- data/lib/jinx/cli/command.rb +214 -0
- data/lib/jinx/helpers/array.rb +108 -0
- data/lib/jinx/helpers/boolean.rb +42 -0
- data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
- data/lib/jinx/helpers/class.rb +149 -0
- data/lib/jinx/helpers/collection.rb +33 -0
- data/lib/jinx/helpers/collections.rb +11 -0
- data/lib/jinx/helpers/collector.rb +20 -0
- data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
- data/lib/jinx/helpers/enumerable.rb +242 -0
- data/lib/jinx/helpers/enumerate.rb +35 -0
- data/lib/jinx/helpers/error.rb +15 -0
- data/lib/jinx/helpers/file_separator.rb +65 -0
- data/lib/jinx/helpers/filter.rb +52 -0
- data/lib/jinx/helpers/flattener.rb +38 -0
- data/lib/jinx/helpers/hash.rb +12 -0
- data/lib/jinx/helpers/hashable.rb +502 -0
- data/lib/jinx/helpers/inflector.rb +36 -0
- data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
- data/lib/jinx/helpers/lazy_hash.rb +44 -0
- data/lib/jinx/helpers/log.rb +106 -0
- data/lib/jinx/helpers/math.rb +12 -0
- data/lib/jinx/helpers/merge.rb +60 -0
- data/lib/jinx/helpers/module.rb +18 -0
- data/lib/jinx/helpers/multi_enumerator.rb +31 -0
- data/lib/jinx/helpers/options.rb +92 -0
- data/lib/jinx/helpers/os.rb +19 -0
- data/lib/jinx/helpers/partial_order.rb +37 -0
- data/lib/jinx/helpers/pretty_print.rb +207 -0
- data/lib/jinx/helpers/set.rb +8 -0
- data/lib/jinx/helpers/stopwatch.rb +76 -0
- data/lib/jinx/helpers/transformer.rb +24 -0
- data/lib/jinx/helpers/transitive_closure.rb +55 -0
- data/lib/jinx/helpers/uniquifier.rb +50 -0
- data/lib/jinx/helpers/validation.rb +33 -0
- data/lib/jinx/helpers/visitor.rb +370 -0
- data/lib/jinx/import/class_path_modifier.rb +77 -0
- data/lib/jinx/import/java.rb +337 -0
- data/lib/jinx/importer.rb +240 -0
- data/lib/jinx/metadata.rb +155 -0
- data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
- data/lib/jinx/metadata/dependency.rb +244 -0
- data/lib/jinx/metadata/id_alias.rb +23 -0
- data/lib/jinx/metadata/introspector.rb +179 -0
- data/lib/jinx/metadata/inverse.rb +170 -0
- data/lib/jinx/metadata/java_property.rb +169 -0
- data/lib/jinx/metadata/propertied.rb +500 -0
- data/lib/jinx/metadata/property.rb +401 -0
- data/lib/jinx/metadata/property_characteristics.rb +114 -0
- data/lib/jinx/resource.rb +862 -0
- data/lib/jinx/resource/copy_visitor.rb +36 -0
- data/lib/jinx/resource/inversible.rb +90 -0
- data/lib/jinx/resource/match_visitor.rb +180 -0
- data/lib/jinx/resource/matcher.rb +20 -0
- data/lib/jinx/resource/merge_visitor.rb +73 -0
- data/lib/jinx/resource/mergeable.rb +185 -0
- data/lib/jinx/resource/reference_enumerator.rb +49 -0
- data/lib/jinx/resource/reference_path_visitor.rb +38 -0
- data/lib/jinx/resource/reference_visitor.rb +55 -0
- data/lib/jinx/resource/unique.rb +35 -0
- data/lib/jinx/version.rb +3 -0
- data/spec/defaults_spec.rb +30 -0
- data/spec/definitions/model/alias/child.rb +5 -0
- data/spec/definitions/model/base/child.rb +5 -0
- data/spec/definitions/model/base/domain_object.rb +5 -0
- data/spec/definitions/model/base/independent.rb +5 -0
- data/spec/definitions/model/defaults/child.rb +5 -0
- data/spec/definitions/model/dependency/child.rb +5 -0
- data/spec/definitions/model/dependency/parent.rb +6 -0
- data/spec/definitions/model/inverse/child.rb +5 -0
- data/spec/definitions/model/inverse/independent.rb +5 -0
- data/spec/definitions/model/inverse/parent.rb +5 -0
- data/spec/definitions/model/mandatory/child.rb +6 -0
- data/spec/dependency_spec.rb +47 -0
- data/spec/family_spec.rb +64 -0
- data/spec/inverse_spec.rb +53 -0
- data/spec/mandatory_spec.rb +43 -0
- data/spec/metadata_spec.rb +68 -0
- data/spec/resource_spec.rb +30 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/model.rb +19 -0
- data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
- data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
- data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
- data/test/fixtures/mixed/ext/build.xml +35 -0
- data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
- data/test/helper.rb +7 -0
- data/test/lib/jinx/command_test.rb +41 -0
- data/test/lib/jinx/helpers/boolean_test.rb +27 -0
- data/test/lib/jinx/helpers/class_test.rb +60 -0
- data/test/lib/jinx/helpers/collections_test.rb +402 -0
- data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
- data/test/lib/jinx/helpers/inflector_test.rb +11 -0
- data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
- data/test/lib/jinx/helpers/module_test.rb +24 -0
- data/test/lib/jinx/helpers/options_test.rb +66 -0
- data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
- data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
- data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
- data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
- data/test/lib/jinx/helpers/visitor_test.rb +288 -0
- data/test/lib/jinx/import/java_test.rb +78 -0
- data/test/lib/jinx/import/mixed_case_test.rb +16 -0
- metadata +272 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'enumerator'
|
|
2
|
+
|
|
3
|
+
class Object
|
|
4
|
+
# This base implementation of +enumerate+ calls the given block on self unless
|
|
5
|
+
# this object in nil. If this object is nil, this method is a no-op.
|
|
6
|
+
#
|
|
7
|
+
# @yield [item] the block to apply to this object
|
|
8
|
+
# @yieldparam item self
|
|
9
|
+
def enumerate
|
|
10
|
+
yield(self) unless nil?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns an enumerator on this Object. This default implementation returns an Enumerable::Enumerator
|
|
14
|
+
# on enumerate.
|
|
15
|
+
#
|
|
16
|
+
# @return [Enumerable] this object as an enumerable item
|
|
17
|
+
def to_enum
|
|
18
|
+
Enumerable::Enumerator.new(self, :enumerate)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module Enumerable
|
|
23
|
+
# Synonym for each.
|
|
24
|
+
#
|
|
25
|
+
# @yield [item] the block to apply to each member
|
|
26
|
+
# @yieldparam item a member of this Enumerable
|
|
27
|
+
def enumerate(&block)
|
|
28
|
+
each(&block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return self
|
|
32
|
+
def to_enum
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Jinx
|
|
2
|
+
# Prints a log message and raises an exception.
|
|
3
|
+
#
|
|
4
|
+
# @param [Class] klass the error class to raise
|
|
5
|
+
# @param [String] msg the error message
|
|
6
|
+
# @param [Exception, nil] cause the exception which caused the error
|
|
7
|
+
def self.fail(klass, msg, cause=nil)
|
|
8
|
+
logger.error(msg)
|
|
9
|
+
if cause then
|
|
10
|
+
logger.error("Caused by: #{cause.class} - #{cause}\n#{cause.backtrace.pp_s}")
|
|
11
|
+
end
|
|
12
|
+
emsg = cause ? "#{msg} - #{$!}" : msg
|
|
13
|
+
raise klass.new(emsg)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require 'jinx/helpers/class'
|
|
2
|
+
|
|
3
|
+
class File
|
|
4
|
+
[:gets, :readline, :readlines].each do |attr|
|
|
5
|
+
# Overrides the standard method to infer a line separator from the input
|
|
6
|
+
# if the separator argument is the default.
|
|
7
|
+
redefine_method(attr) do |original|
|
|
8
|
+
lambda do |*params|
|
|
9
|
+
sep = params.first || default_line_separator
|
|
10
|
+
send(original, sep)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Overrides the standard {#each} method to infer a line separator from the input
|
|
15
|
+
# if the separator argument is not given.
|
|
16
|
+
def each(separator=nil)
|
|
17
|
+
while (line = gets(separator)) do
|
|
18
|
+
yield line
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Returns the default line separator. The logic is borrowed from the FasterCVS gem.
|
|
26
|
+
def default_line_separator
|
|
27
|
+
@def_line_sep ||= infer_line_separator
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def infer_line_separator
|
|
31
|
+
type_line_separator or content_line_separator or $/
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def type_line_separator
|
|
35
|
+
if [ARGF, STDIN, STDOUT, STDERR].include?(self) or
|
|
36
|
+
(defined?(Zlib) and self.class == Zlib::GzipWriter) then
|
|
37
|
+
return $/
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def content_line_separator
|
|
42
|
+
begin
|
|
43
|
+
saved_pos = pos # remember where we were
|
|
44
|
+
# read a chunk until a separator is discovered
|
|
45
|
+
sep = discover_line_separator
|
|
46
|
+
# tricky seek() clone to work around GzipReader's lack of seek()
|
|
47
|
+
rewind
|
|
48
|
+
# reset back to the remembered position
|
|
49
|
+
chunks, residual = saved_pos.divmod(1024)
|
|
50
|
+
chunks.times { read(1024) }
|
|
51
|
+
read(residual)
|
|
52
|
+
rescue IOError # stream not opened for reading
|
|
53
|
+
end
|
|
54
|
+
sep
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def discover_line_separator
|
|
58
|
+
# read a chunk until a separator is discovered
|
|
59
|
+
while (sample = read(1024)) do
|
|
60
|
+
sample += read(1) if sample[-1, 1] == "\r" and not eof?
|
|
61
|
+
# try to find a standard separator
|
|
62
|
+
return $& if sample =~ /\r\n?|\n/
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Jinx
|
|
2
|
+
# This Filter helper class applies a selection block to a base enumeration.
|
|
3
|
+
class Filter
|
|
4
|
+
include Collection
|
|
5
|
+
|
|
6
|
+
def initialize(enum=[], &filter)
|
|
7
|
+
@base = enum
|
|
8
|
+
@filter = filter
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Calls the given block on each item which passes this Filter's filter test.
|
|
12
|
+
#
|
|
13
|
+
# @yield [item] the block called on each item
|
|
14
|
+
# @yieldparam item the enumerated item
|
|
15
|
+
def each
|
|
16
|
+
@base.each { |item| yield(item) if @filter ? @filter.call(item) : item }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Optimized for a Set base.
|
|
20
|
+
#
|
|
21
|
+
# @param [item] the item to check
|
|
22
|
+
# @return [Boolean] whether the item is a member of this Enumerable
|
|
23
|
+
def include?(item)
|
|
24
|
+
return false if Set === @base and not @base.include?(item)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Adds an item to the base Enumerable, if this Filter's base supports it.
|
|
29
|
+
#
|
|
30
|
+
# @param item the item to add
|
|
31
|
+
# @return [Filter] self
|
|
32
|
+
def <<(item)
|
|
33
|
+
@base << item
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param [Enumerable] other the Enumerable to merge
|
|
38
|
+
# @return [Array] this Filter's filtered content merged with the other Enumerable
|
|
39
|
+
def merge(other)
|
|
40
|
+
to_a.merge!(other)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Merges the other Enumerable into the base Enumerable, if the base supports it.
|
|
44
|
+
#
|
|
45
|
+
# @param other (see #merge)
|
|
46
|
+
# @return [Filter, nil] this Filter's filtered content merged with the other Enumerable
|
|
47
|
+
def merge!(other)
|
|
48
|
+
@base.merge!(other)
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Jinx
|
|
2
|
+
# A Flattener applies a given block to flattened collection content.
|
|
3
|
+
class Flattener
|
|
4
|
+
include Collection
|
|
5
|
+
|
|
6
|
+
# Visits the enumerated items in the given object's flattened content.
|
|
7
|
+
# block is called on the base itself if the base is neither nil nor a Enumerable.
|
|
8
|
+
# If the base object is nil or empty, then this method is a no-op and returns nil.
|
|
9
|
+
def self.on(obj, &block)
|
|
10
|
+
obj.collection? ? obj.each { |item| on(item, &block) } : yield(obj) unless obj.nil?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Initializes a new Flattener on the given object.
|
|
14
|
+
#
|
|
15
|
+
# @param obj the Enumerable or non-collection object
|
|
16
|
+
def initialize(obj)
|
|
17
|
+
@base = obj
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Calls the the given block on this Flattener's flattened content.
|
|
21
|
+
# If the base object is a collection, then the block is called on the flattened content.
|
|
22
|
+
# If the base object is nil, then this method is a no-op.
|
|
23
|
+
# If the base object is neither nil nor a collection, then the block given to this method
|
|
24
|
+
# is called on the base object itself.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# Flattener.new(nil).each { |n| print n } #=>
|
|
28
|
+
# Flattener.new(1).each { |n| print n } #=> 1
|
|
29
|
+
# Flattener.new([1, [2, 3]]).each { |n| print n } #=> 123
|
|
30
|
+
def each(&block)
|
|
31
|
+
Flattener.on(@base, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_s
|
|
35
|
+
to_a.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'jinx/helpers/hashable'
|
|
2
|
+
|
|
3
|
+
class Hash
|
|
4
|
+
include Jinx::Hashable
|
|
5
|
+
|
|
6
|
+
# The EMPTY_HASH constant is an immutable empty hash, used primarily as a default argument.
|
|
7
|
+
class << EMPTY_HASH ||= Hash.new
|
|
8
|
+
def []=(key, value)
|
|
9
|
+
Jinx.fail(NotImplementedError, "Modification of the constant empty hash is not supported")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
require 'jinx/helpers/collection'
|
|
2
|
+
require 'jinx/helpers/hashable'
|
|
3
|
+
|
|
4
|
+
module Jinx
|
|
5
|
+
# Hashable is a Hash mixin that adds utility methods to a Hash.
|
|
6
|
+
# Hashable can be included by any class or module which implements an _each_ method
|
|
7
|
+
# with arguments _key_ and _value_.
|
|
8
|
+
module Hashable
|
|
9
|
+
include Collection
|
|
10
|
+
|
|
11
|
+
# @see Hash#each_pair
|
|
12
|
+
def each_pair(&block)
|
|
13
|
+
each(&block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @see Hash#[]
|
|
17
|
+
def [](key)
|
|
18
|
+
detect_value { |k, v| v if k == key }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @see Hash#each_key
|
|
22
|
+
def each_key
|
|
23
|
+
each { |k, v| yield k }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @yield [key] the detector block
|
|
27
|
+
# @yieldparam key the hash key
|
|
28
|
+
# @return the key for which the detector block returns a non-nil, non-false value,
|
|
29
|
+
# or nil if none
|
|
30
|
+
# @example
|
|
31
|
+
# {1 => :a, 2 => :b, 3 => :c}.detect_key { |k| k > 1 } #=> 2
|
|
32
|
+
def detect_key
|
|
33
|
+
each_key { |k| return k if yield k }
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def has_key?(key)
|
|
38
|
+
!!detect_key { |k| k == key }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @yield [value] the detector block
|
|
42
|
+
# @yieldparam value the hash value
|
|
43
|
+
# @return the key for which the detector block returns a non-nil, non-false value,
|
|
44
|
+
# or nil if none
|
|
45
|
+
# @example
|
|
46
|
+
# {:a => 1, :b => 2, :c => 3}.detect_key_with_value { |v| v > 1 } #=> :b
|
|
47
|
+
def detect_key_with_value
|
|
48
|
+
each { |k, v| return k if yield v }
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @see Hash#each_value
|
|
53
|
+
def each_value
|
|
54
|
+
each { |k, v| yield v }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns a Hashable which composes each value in this Hashable with the key of
|
|
58
|
+
# the other Hashable, e.g.:
|
|
59
|
+
# x = {:a => :c, :b => :d}
|
|
60
|
+
# y = {:c => 1}
|
|
61
|
+
# z = x.compose(y)
|
|
62
|
+
# z[:a] #=> {:c => 1}
|
|
63
|
+
# z[:b] #=> nil
|
|
64
|
+
#
|
|
65
|
+
# The accessor reflects changes to the underlying hashes, e.g. given the above example:
|
|
66
|
+
# x[:b] = 2
|
|
67
|
+
# z[:b] #=> {:c => 1}
|
|
68
|
+
#
|
|
69
|
+
# Update operations on the result are not supported.
|
|
70
|
+
#
|
|
71
|
+
# @param [Hashable] other the Hashable to compose with this Hashable
|
|
72
|
+
# @return [Hashable] the composed result
|
|
73
|
+
def compose(other)
|
|
74
|
+
transform_value { |v| {v => other[v]} if other.has_key?(v) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns a Hashable which joins each value in this Hashable with the key of
|
|
78
|
+
# the other Hashable, e.g.:
|
|
79
|
+
# x = {:a => :c, :b => :d}
|
|
80
|
+
# y = {:c => 1}
|
|
81
|
+
# z = x.join(y)
|
|
82
|
+
# z[:a] #=> 1
|
|
83
|
+
# z[:b] #=> nil
|
|
84
|
+
#
|
|
85
|
+
# The accessor reflects changes to the underlying hashes, e.g. given the above example:
|
|
86
|
+
# x[:b] = 2
|
|
87
|
+
# z[:b] #=> 2
|
|
88
|
+
#
|
|
89
|
+
# Update operations on the result are not supported.
|
|
90
|
+
#
|
|
91
|
+
# @param [Hashable] other the Hashable to join with this Hashable
|
|
92
|
+
# @return [Hashable] the joined result
|
|
93
|
+
def join(other)
|
|
94
|
+
transform_value { |v| other[v] }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns a Hashable which associates each key of both this Hashable and the other Hashable
|
|
98
|
+
# with the corresponding value in the first Hashable which has that key, e.g.:
|
|
99
|
+
# x = {:a => 1, :b => 2}
|
|
100
|
+
# y = {:b => 3, :c => 4}
|
|
101
|
+
# z = x + y
|
|
102
|
+
# z[:b] #=> 2
|
|
103
|
+
#
|
|
104
|
+
# The accessor reflects changes to the underlying hashes, e.g. given the above example:
|
|
105
|
+
# x.delete(:b)
|
|
106
|
+
# z[:b] #=> 3
|
|
107
|
+
#
|
|
108
|
+
# Update operations on the result are not supported.
|
|
109
|
+
#
|
|
110
|
+
# @param [Hashable] other the Hashable to form a union with this Hashable
|
|
111
|
+
# @return [Hashable] the union result
|
|
112
|
+
def union(other)
|
|
113
|
+
MultiHash.new(self, other)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
alias :+ :union
|
|
117
|
+
|
|
118
|
+
# Returns a new Hashable that iterates over the base Hashable <key, value> pairs for which the block
|
|
119
|
+
# given to this method evaluates to a non-nil, non-false value, e.g.:
|
|
120
|
+
# {:a => 1, :b => 2, :c => 3}.filter { |k, v| k != :b }.to_hash #=> {:a => 1, :c => 3}
|
|
121
|
+
#
|
|
122
|
+
# The default filter block tests the value, e.g.:
|
|
123
|
+
# {:a => 1, :b => nil}.filter.to_hash #=> {:a => 1}
|
|
124
|
+
#
|
|
125
|
+
# @yield [key, value] the filter block
|
|
126
|
+
# @return [Hashable] the filtered result
|
|
127
|
+
def filter(&block)
|
|
128
|
+
Filter.new(self, &block)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Optimization of {#filter} for a block that only uses the key.
|
|
132
|
+
#
|
|
133
|
+
# @example
|
|
134
|
+
# {:a => 1, :b => 2, :c => 3}.filter_on_key { |k| k != :b }.to_hash #=> {:a => 1, :c => 3}
|
|
135
|
+
#
|
|
136
|
+
# @yield [key] the filter block
|
|
137
|
+
# @yieldparam key the hash key to filter
|
|
138
|
+
# @return [Hashable] the filtered result
|
|
139
|
+
def filter_on_key(&block)
|
|
140
|
+
KeyFilter.new(self, &block)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Hashable] a {#filter} that only uses the value.
|
|
144
|
+
# @yield [value] the filter block
|
|
145
|
+
# @yieldparam value the hash value to filter
|
|
146
|
+
# @return [Hashable] the filtered result
|
|
147
|
+
def filter_on_value
|
|
148
|
+
filter { |k, v| yield v }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @return [Hash] a {#filter} of this Hashable which excludes the entries with a null value
|
|
152
|
+
def compact
|
|
153
|
+
filter_on_value { |v| not v.nil? }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns the difference between this Hashable and the other Hashable in a Hash of the form:
|
|
157
|
+
#
|
|
158
|
+
# _key_ => [_mine_, _theirs_]
|
|
159
|
+
#
|
|
160
|
+
# where:
|
|
161
|
+
# * _key_ is the key of association which differs
|
|
162
|
+
# * _mine_ is the value for _key_ in this hash
|
|
163
|
+
# * _theirs_ is the value for _key_ in the other hash
|
|
164
|
+
#
|
|
165
|
+
# @param [Hashable] other the Hashable to subtract
|
|
166
|
+
# @yield [key, v1, v2] the optional block which determines whether values differ (default is equality)
|
|
167
|
+
# @yieldparam key the key for which values are compared
|
|
168
|
+
# @yieldparam v1 the value for key from this Hashable
|
|
169
|
+
# @yieldparam v2 the value for key from the other Hashable
|
|
170
|
+
# @return [{Object => (Object,Object)}] a hash of the differences
|
|
171
|
+
def diff(other)
|
|
172
|
+
(keys.to_set + other.keys).to_compact_hash do |k|
|
|
173
|
+
mine = self[k]
|
|
174
|
+
yours = other[k]
|
|
175
|
+
[mine, yours] unless block_given? ? yield(k, mine, yours) : mine == yours
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @yield [key1, key2] the key sort block
|
|
180
|
+
# @return [Hashable] a hash whose #each and {#each_pair} enumerations are sorted by key
|
|
181
|
+
def sort(&sorter)
|
|
182
|
+
SortedHash.new(self, &sorter)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Returns a hash which associates each key in this hash with the value mapped by the others.
|
|
186
|
+
#
|
|
187
|
+
# @example
|
|
188
|
+
# {:a => 1, :b => 2}.assoc_values({:a => 3, :c => 4}) #=> {:a => [1, 3], :b => [2, nil], :c => [nil, 4]}
|
|
189
|
+
# {:a => 1, :b => 2}.assoc_values({:a => 3}, {:a => 4, :b => 5}) #=> {:a => [1, 3, 4], :b => [2, nil, 5]}
|
|
190
|
+
#
|
|
191
|
+
# @param [<Hashable>] others the other Hashables to associate with this Hashable
|
|
192
|
+
# @return [Hash] the association hash
|
|
193
|
+
def assoc_values(*others)
|
|
194
|
+
all_keys = keys
|
|
195
|
+
others.each { |hash| all_keys.concat(hash.keys) }
|
|
196
|
+
all_keys.to_compact_hash do |k|
|
|
197
|
+
others.map { |other| other[k] }.unshift(self[k])
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Returns an Enumerable whose each block is called on each key which maps to a value which
|
|
202
|
+
# either equals the given target_value or satisfies the filter block.
|
|
203
|
+
#
|
|
204
|
+
# @param target_value the filter value
|
|
205
|
+
# @yield [value] the filter block
|
|
206
|
+
# @return [Enumerable] the filtered keys
|
|
207
|
+
def enum_keys_with_value(target_value=nil, &filter) # :yields: value
|
|
208
|
+
return enum_keys_with_value { |v| v == target_value } if target_value
|
|
209
|
+
filter_on_value(&filter).keys
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @return [Enumerable] Enumerable over this Hashable's keys
|
|
213
|
+
def enum_keys
|
|
214
|
+
Enumerable::Enumerator.new(self, :each_key)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @return [Array] this Hashable's keys
|
|
218
|
+
def keys
|
|
219
|
+
enum_keys.to_a
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# @param key search target
|
|
223
|
+
# @return [Boolean] whether this Hashable has the given key
|
|
224
|
+
def has_key?(key)
|
|
225
|
+
enum_keys.include?(key)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
alias :include? :has_key?
|
|
229
|
+
|
|
230
|
+
# @return [Enumerable] an Enumerable over this Hashable's values
|
|
231
|
+
def enum_values
|
|
232
|
+
Enumerable::Enumerator.new(self, :each_value)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# @yield [key] the key selector
|
|
236
|
+
# @return [Enumerable] the keys which satisfy the block given to this method
|
|
237
|
+
def select_keys(&block)
|
|
238
|
+
enum_keys.select(&block)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# @yield [key] the key rejector
|
|
242
|
+
# @return [Enumerable] the keys which do not satisfy the block given to this method
|
|
243
|
+
def reject_keys(&block)
|
|
244
|
+
enum_keys.reject(&block)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# @yield [value] the value selector
|
|
248
|
+
# @return [Enumerable] the values which satisfy the block given to this method
|
|
249
|
+
def select_values(&block)
|
|
250
|
+
enum_values.select(&block)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# @yield [value] the value rejector
|
|
254
|
+
# @return [Enumerable] the values which do not satisfy the block given to this method
|
|
255
|
+
def reject_values(&block)
|
|
256
|
+
enum_values.reject(&block)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# @return [Array] this Enumerable's values
|
|
260
|
+
def values
|
|
261
|
+
enum_values.to_a
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @param value search target
|
|
265
|
+
# @return [Boolean] whether this Hashable has the given value
|
|
266
|
+
def has_value?(value)
|
|
267
|
+
enum_values.include?(value)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @return [Array] a flattened Array of this Hash
|
|
271
|
+
# @example
|
|
272
|
+
# {:a => {:b => :c}, :d => :e, :f => [:g]} #=> [:a, :b, :c, :d, :e, :f, :g]
|
|
273
|
+
def flatten
|
|
274
|
+
Flattener.new(self).to_a
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# @yield [key, value] hash splitter
|
|
278
|
+
# @return [(Hash, Hash)] two hashes split by whether calling the block on the
|
|
279
|
+
# entry returns a non-nil, non-false value
|
|
280
|
+
# @example
|
|
281
|
+
# {:a => 1, :b => 2}.split { |k, v| v < 2 } #=> [{:a => 1}, {:b => 2}]
|
|
282
|
+
def split(&block)
|
|
283
|
+
partition(&block).map { |pairs| pairs.to_assoc_hash }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Returns a new Hash that recursively copies this hash's values. Values of type hash are copied using copy_recursive.
|
|
287
|
+
# Other values are unchanged.
|
|
288
|
+
#
|
|
289
|
+
# This method is useful for preserving and restoring hash associations.
|
|
290
|
+
#
|
|
291
|
+
# @return [Hash] a deep copy of this Hashable
|
|
292
|
+
def copy_recursive
|
|
293
|
+
copy = Hash.new
|
|
294
|
+
keys.each do |k|
|
|
295
|
+
value = self[k]
|
|
296
|
+
copy[k] = Hash === value ? value.copy_recursive : value
|
|
297
|
+
end
|
|
298
|
+
copy
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# @example
|
|
302
|
+
# {:a => 1, :b => 2}.transform_value { |n| n * 2 }.values #=> [2, 4]
|
|
303
|
+
#
|
|
304
|
+
# @yield [value] transforms the given value
|
|
305
|
+
# @yieldparam [value] the value to transform
|
|
306
|
+
# @return [Hash] a new Hash that transforms each value
|
|
307
|
+
def transform_value(&transformer)
|
|
308
|
+
ValueTransformerHash.new(self, &transformer)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# @example
|
|
312
|
+
# {1 => :a, 2 => :b}.transform_key { |n| n * 2 }.keys #=> [2, 4]
|
|
313
|
+
#
|
|
314
|
+
# @yield [key] transforms the given key
|
|
315
|
+
# @yieldparam [value] the key to transform
|
|
316
|
+
# @return [Hash] a new Hash that transforms each key
|
|
317
|
+
def transform_key(&transformer)
|
|
318
|
+
KeyTransformerHash.new(self, &transformer)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# @return [Hash] a new Hash created from this Hashable's content
|
|
322
|
+
def to_hash
|
|
323
|
+
hash = {}
|
|
324
|
+
each { |k, v| hash[k] = v }
|
|
325
|
+
hash
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def to_set
|
|
329
|
+
to_a.to_set
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def to_s
|
|
333
|
+
to_hash.to_s
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def inspect
|
|
337
|
+
to_hash.inspect
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def ==(other)
|
|
341
|
+
to_hash == other.to_hash rescue super
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private
|
|
345
|
+
|
|
346
|
+
# @see #filter
|
|
347
|
+
class Filter
|
|
348
|
+
include Hashable
|
|
349
|
+
|
|
350
|
+
def initialize(base, &filter)
|
|
351
|
+
@base = base
|
|
352
|
+
@filter = filter
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def each
|
|
356
|
+
@base.each { |k, v| yield(k, v) if @filter ? @filter.call(k, v) : v }
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# @see #filter_on_key
|
|
361
|
+
class KeyFilter < Filter
|
|
362
|
+
include Hashable
|
|
363
|
+
|
|
364
|
+
def initialize(base)
|
|
365
|
+
super(base) { |k, v| yield(k) }
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def [](key)
|
|
369
|
+
super if @filter.call(key, nil)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# @see #sort
|
|
374
|
+
class SortedHash
|
|
375
|
+
include Hashable
|
|
376
|
+
|
|
377
|
+
def initialize(base, &comparator)
|
|
378
|
+
@base = base
|
|
379
|
+
@comparator = comparator
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def each
|
|
383
|
+
@base.keys.sort { |k1, k2| @comparator ? @comparator.call(k1, k2) : k1 <=> k2 }.each { |k| yield(k, @base[k]) }
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Combines hashes. See Hash#+ for details.
|
|
388
|
+
class MultiHash
|
|
389
|
+
include Hashable
|
|
390
|
+
|
|
391
|
+
# @return [<Hashable>] the enumerated hashes
|
|
392
|
+
attr_reader :components
|
|
393
|
+
|
|
394
|
+
def initialize(*hashes)
|
|
395
|
+
if hashes.include?(nil) then Jinx.fail(ArgumentError, "MultiHash is missing a component hash.") end
|
|
396
|
+
@components = hashes
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def [](key)
|
|
400
|
+
@components.each { |hash| return hash[key] if hash.has_key?(key) }
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def has_key?(key)
|
|
405
|
+
@components.any? { |hash| hash.has_key?(key) }
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def has_value?(value)
|
|
409
|
+
@components.any? { |hash| hash.has_value?(value) }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def each
|
|
413
|
+
@components.each_with_index do |hash, index|
|
|
414
|
+
hash.each do |k, v|
|
|
415
|
+
yield(k, v) unless (0...index).any? { |i| @components[i].has_key?(k) }
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
self
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# The ValueTransformerHash class pipes the value from a base Hashable into a transformer block.
|
|
424
|
+
# @private
|
|
425
|
+
class ValueTransformerHash
|
|
426
|
+
include Hashable
|
|
427
|
+
|
|
428
|
+
# Creates a ValueTransformerHash on the base hash and value transformer block.
|
|
429
|
+
#
|
|
430
|
+
# @param [Hash, nil] base the hash to transform
|
|
431
|
+
# @yield [value] transforms the base value
|
|
432
|
+
# @yieldparam value the base value to transform
|
|
433
|
+
def initialize(base, &transformer)
|
|
434
|
+
@base = base
|
|
435
|
+
@xfm = transformer
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# @param key the hash key
|
|
439
|
+
# @return the value at key after this ValueTransformerHash's transformer block is applied, or nil
|
|
440
|
+
# if this hash does not contain key
|
|
441
|
+
def [](key)
|
|
442
|
+
@xfm.call(@base[key]) if @base.has_key?(key)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# @yield [key, value] operate on the key and transformed value
|
|
446
|
+
# @yieldparam key the hash key
|
|
447
|
+
# @yieldparam value the transformed hash value
|
|
448
|
+
def each
|
|
449
|
+
@base.each { |k, v| yield(k, @xfm.call(v)) }
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# The KeyTransformerHash class pipes the key from a base Hashable into a transformer block.
|
|
454
|
+
# @private
|
|
455
|
+
class KeyTransformerHash
|
|
456
|
+
include Hashable
|
|
457
|
+
|
|
458
|
+
# Creates a KeyTransformerHash on the base hash and key transformer block.
|
|
459
|
+
#
|
|
460
|
+
# @param [Hash, nil] base the hash to transform
|
|
461
|
+
# @yield [key] transforms the base key
|
|
462
|
+
# @yieldparam key the base key to transform
|
|
463
|
+
def initialize(base, &transformer)
|
|
464
|
+
@base = base
|
|
465
|
+
@xfm = transformer
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# @param key the untransformed hash key
|
|
469
|
+
# @return the value for the transformed key
|
|
470
|
+
def [](key)
|
|
471
|
+
@base[@xfm.call(@base[key])]
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# @yield [key, value] operate on the transformed key and value
|
|
475
|
+
# @yieldparam key the transformed hash key
|
|
476
|
+
# @yieldparam value the hash value
|
|
477
|
+
def each
|
|
478
|
+
@base.each { |k, v| yield(@xfm.call(k), v) }
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Hashinator creates a Hashable from an Enumerable on [_key_, _value_] pairs.
|
|
483
|
+
# The Hashinator reflects changes to the underlying Enumerable.
|
|
484
|
+
#
|
|
485
|
+
# @example
|
|
486
|
+
# base = [[:a, 1], [:b, 2]]
|
|
487
|
+
# hash = Hashinator.new(base)
|
|
488
|
+
# hash[:a] #=> 1
|
|
489
|
+
# base.first[1] = 3
|
|
490
|
+
# hash[:a] #=> 3
|
|
491
|
+
class Hashinator
|
|
492
|
+
include Hashable
|
|
493
|
+
|
|
494
|
+
def initialize(enum)
|
|
495
|
+
@base = enum
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def each
|
|
499
|
+
@base.each { |pair| yield(*pair) }
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|