fusu 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/fusu.rb +6 -0
- data/lib/fusu/concern.rb +142 -0
- data/lib/fusu/hash.rb +12 -0
- data/lib/fusu/hash/deep_merge.rb +37 -0
- data/lib/fusu/hash/except.rb +25 -0
- data/lib/fusu/hash/keys.rb +150 -0
- data/lib/fusu/hash/reverse_merge.rb +26 -0
- data/lib/fusu/hash_with_indifferent_access.rb +288 -0
- data/lib/fusu/regexp.rb +12 -0
- data/lib/fusu/string.rb +6 -0
- data/lib/fusu/string/methods.rb +348 -0
- data/lib/fusu/try.rb +20 -0
- data/lib/fusu/version.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b0df9b35cbcd397ff4eaa1702330e0e0255f84a
|
4
|
+
data.tar.gz: 40b28b9e12efa5cf79f00171311c62e3616e0a37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 028aaafaf603c46939c503d0f4b422fc4649503968d620b950e647f1b0ae4f33dce8d0dd5b7839e58d48bbc5cc6df1e79038c6b9d8478619a5ad5b4b81e34ad1
|
7
|
+
data.tar.gz: 28e4994d51bcdb01fd825e0382f5e4c312006a649872443a5ccb31f8ba3a5ff2bf222d86d0e53f40bf846926a644d1b35cb2558cd8f074a24c6d3eb809cec4b2
|
data/lib/fusu.rb
CHANGED
data/lib/fusu/concern.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
module Fusu
|
2
|
+
# A typical module looks like this:
|
3
|
+
#
|
4
|
+
# module M
|
5
|
+
# def self.included(base)
|
6
|
+
# base.extend ClassMethods
|
7
|
+
# base.class_eval do
|
8
|
+
# scope :disabled, -> { where(disabled: true) }
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# module ClassMethods
|
13
|
+
# ...
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# By using <tt>Fusu::Concern</tt> the above module could instead be
|
18
|
+
# written as:
|
19
|
+
#
|
20
|
+
# require 'active_support/concern'
|
21
|
+
#
|
22
|
+
# module M
|
23
|
+
# extend Fusu::Concern
|
24
|
+
#
|
25
|
+
# included do
|
26
|
+
# scope :disabled, -> { where(disabled: true) }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class_methods do
|
30
|
+
# ...
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
35
|
+
# and a +Bar+ module which depends on the former, we would typically write the
|
36
|
+
# following:
|
37
|
+
#
|
38
|
+
# module Foo
|
39
|
+
# def self.included(base)
|
40
|
+
# base.class_eval do
|
41
|
+
# def self.method_injected_by_foo
|
42
|
+
# ...
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# module Bar
|
49
|
+
# def self.included(base)
|
50
|
+
# base.method_injected_by_foo
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# class Host
|
55
|
+
# include Foo # We need to include this dependency for Bar
|
56
|
+
# include Bar # Bar is the module that Host really needs
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
60
|
+
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
61
|
+
#
|
62
|
+
# module Bar
|
63
|
+
# include Foo
|
64
|
+
# def self.included(base)
|
65
|
+
# base.method_injected_by_foo
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# class Host
|
70
|
+
# include Bar
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
74
|
+
# is the +Bar+ module, not the +Host+ class. With <tt>Fusu::Concern</tt>,
|
75
|
+
# module dependencies are properly resolved:
|
76
|
+
#
|
77
|
+
# require 'active_support/concern'
|
78
|
+
#
|
79
|
+
# module Foo
|
80
|
+
# extend Fusu::Concern
|
81
|
+
# included do
|
82
|
+
# def self.method_injected_by_foo
|
83
|
+
# ...
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# module Bar
|
89
|
+
# extend Fusu::Concern
|
90
|
+
# include Foo
|
91
|
+
#
|
92
|
+
# included do
|
93
|
+
# self.method_injected_by_foo
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# class Host
|
98
|
+
# include Bar # It works, now Bar takes care of its dependencies
|
99
|
+
# end
|
100
|
+
module Concern
|
101
|
+
class MultipleIncludedBlocks < StandardError #:nodoc:
|
102
|
+
def initialize
|
103
|
+
super "Cannot define multiple 'included' blocks for a Concern"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.extended(base) #:nodoc:
|
108
|
+
base.instance_variable_set(:@_dependencies, [])
|
109
|
+
end
|
110
|
+
|
111
|
+
def append_features(base)
|
112
|
+
if base.instance_variable_defined?(:@_dependencies)
|
113
|
+
base.instance_variable_get(:@_dependencies) << self
|
114
|
+
return false
|
115
|
+
else
|
116
|
+
return false if base < self
|
117
|
+
@_dependencies.each { |dep| base.send(:include, dep) }
|
118
|
+
super
|
119
|
+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
120
|
+
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def included(base = nil, &block)
|
125
|
+
if base.nil?
|
126
|
+
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
127
|
+
|
128
|
+
@_included_block = block
|
129
|
+
else
|
130
|
+
super
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def class_methods(&class_methods_module_definition)
|
135
|
+
mod = const_defined?(:ClassMethods, false) ?
|
136
|
+
const_get(:ClassMethods) :
|
137
|
+
const_set(:ClassMethods, Module.new)
|
138
|
+
|
139
|
+
mod.module_eval(&class_methods_module_definition)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/fusu/hash.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Fusu
|
3
|
+
module Hash
|
4
|
+
module DeepMerge
|
5
|
+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
6
|
+
#
|
7
|
+
# h1 = { a: true, b: { c: [1, 2, 3] } }
|
8
|
+
# h2 = { a: false, b: { x: [3, 4, 5] } }
|
9
|
+
#
|
10
|
+
# Fusu::Hash.deep_merge(h1, h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
|
11
|
+
#
|
12
|
+
# Like with Hash#merge in the standard library, a block can be provided
|
13
|
+
# to merge values:
|
14
|
+
#
|
15
|
+
# h1 = { a: 100, b: 200, c: { c1: 100 } }
|
16
|
+
# h2 = { b: 250, c: { c1: 200 } }
|
17
|
+
# Fusu::Hash.deep_merge(h1, h2) { |key, this_val, other_val| this_val + other_val }
|
18
|
+
# # => { a: 100, b: 450, c: { c1: 300 } }
|
19
|
+
def deep_merge(hash, other_hash, &block)
|
20
|
+
deep_merge!(hash.dup, other_hash, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Same as +deep_merge+, but modifies +self+.
|
24
|
+
def deep_merge!(hash, other_hash, &block)
|
25
|
+
hash.merge!(other_hash) do |key, this_val, other_val|
|
26
|
+
if this_val.class <= ::Hash && other_val.class <= ::Hash
|
27
|
+
deep_merge(this_val, other_val, &block)
|
28
|
+
elsif block_given?
|
29
|
+
block.call(key, this_val, other_val)
|
30
|
+
else
|
31
|
+
other_val
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Fusu
|
2
|
+
module Hash
|
3
|
+
module Except
|
4
|
+
# Returns a hash that includes everything but the given keys.
|
5
|
+
# hash = { a: true, b: false, c: nil}
|
6
|
+
# Fusu::Hash.except(hash, :c) # => { a: true, b: false}
|
7
|
+
# hash # => { a: true, b: false, c: nil}
|
8
|
+
#
|
9
|
+
# This is useful for limiting a set of parameters to everything but a few known toggles:
|
10
|
+
# @person.update(Fusu::Hash.except(params[:person], :admin))
|
11
|
+
def except(hash, *keys)
|
12
|
+
except!(hash.dup, *keys)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Replaces the hash without the given keys.
|
16
|
+
# hash = { a: true, b: false, c: nil}
|
17
|
+
# Fusu::Hash.except!(hash, :c) # => { a: true, b: false}
|
18
|
+
# hash # => { a: true, b: false }
|
19
|
+
def except!(hash, *keys)
|
20
|
+
keys.each { |key| hash.delete(key) }
|
21
|
+
hash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Fusu
|
2
|
+
module Hash
|
3
|
+
module Keys
|
4
|
+
# Returns a new hash with all keys converted using the block operation.
|
5
|
+
#
|
6
|
+
# hash = { name: 'Rob', age: '28' }
|
7
|
+
#
|
8
|
+
# Fusu::Hash.transform_keys(hash){ |key| key.to_s.upcase }
|
9
|
+
# # => {"NAME"=>"Rob", "AGE"=>"28"}
|
10
|
+
def transform_keys(hash)
|
11
|
+
return enum_for(:transform_keys, hash) unless block_given?
|
12
|
+
result = ::Hash.new
|
13
|
+
hash.each_key do |key|
|
14
|
+
result[yield(key)] = hash[key]
|
15
|
+
end
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
# Destructively convert all keys using the block operations.
|
20
|
+
# Same as transform_keys but modifies +self+.
|
21
|
+
def transform_keys!(hash)
|
22
|
+
return enum_for(:transform_keys!, hash) unless block_given?
|
23
|
+
hash.keys.each do |key|
|
24
|
+
hash[yield(key)] = hash.delete(key)
|
25
|
+
end
|
26
|
+
hash
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a new hash with all keys converted to strings.
|
30
|
+
#
|
31
|
+
# hash = { name: 'Rob', age: '28' }
|
32
|
+
#
|
33
|
+
# Fusu::Hash.stringify_keys(hash)
|
34
|
+
# # => {"name"=>"Rob", "age"=>"28"}
|
35
|
+
def stringify_keys(hash)
|
36
|
+
transform_keys(hash){ |key| key.to_s }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Destructively convert all keys to strings. Same as
|
40
|
+
# +stringify_keys+, but modifies +self+.
|
41
|
+
def stringify_keys!(hash)
|
42
|
+
transform_keys!(hash){ |key| key.to_s }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
46
|
+
# they respond to +to_sym+.
|
47
|
+
#
|
48
|
+
# hash = { 'name' => 'Rob', 'age' => '28' }
|
49
|
+
#
|
50
|
+
# Fusu::Hash.symbolize_keys(hash)
|
51
|
+
# # => {:name=>"Rob", :age=>"28"}
|
52
|
+
def symbolize_keys(hash)
|
53
|
+
transform_keys(hash){ |key| key.to_sym rescue key }
|
54
|
+
end
|
55
|
+
alias_method :to_options, :symbolize_keys
|
56
|
+
|
57
|
+
# Destructively convert all keys to symbols, as long as they respond
|
58
|
+
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
|
59
|
+
def symbolize_keys!(hash)
|
60
|
+
transform_keys!(hash){ |key| key.to_sym rescue key }
|
61
|
+
end
|
62
|
+
alias_method :to_options!, :symbolize_keys!
|
63
|
+
|
64
|
+
# Returns a new hash with all keys converted by the block operation.
|
65
|
+
# This includes the keys from the root hash and from all
|
66
|
+
# nested hashes and arrays.
|
67
|
+
#
|
68
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
69
|
+
#
|
70
|
+
# Fusu::Hash.deep_transform_keys(hash){ |key| key.to_s.upcase }
|
71
|
+
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
|
72
|
+
def deep_transform_keys(hash, &block)
|
73
|
+
_deep_transform_keys_in_object(hash, &block)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Destructively convert all keys by using the block operation.
|
77
|
+
# This includes the keys from the root hash and from all
|
78
|
+
# nested hashes and arrays.
|
79
|
+
def deep_transform_keys!(hash, &block)
|
80
|
+
_deep_transform_keys_in_object!(hash, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns a new hash with all keys converted to strings.
|
84
|
+
# This includes the keys from the root hash and from all
|
85
|
+
# nested hashes and arrays.
|
86
|
+
#
|
87
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
88
|
+
#
|
89
|
+
# Fusu::Hash.deep_stringify_keys(hash)
|
90
|
+
# # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
|
91
|
+
def deep_stringify_keys(hash)
|
92
|
+
deep_transform_keys(hash){ |key| key.to_s }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Destructively convert all keys to strings.
|
96
|
+
# This includes the keys from the root hash and from all
|
97
|
+
# nested hashes and arrays.
|
98
|
+
def deep_stringify_keys!(hash)
|
99
|
+
deep_transform_keys!(hash){ |key| key.to_s }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
103
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
104
|
+
# and from all nested hashes and arrays.
|
105
|
+
#
|
106
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
107
|
+
#
|
108
|
+
# Fusu::Hash.deep_symbolize_keys(hash)
|
109
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
110
|
+
def deep_symbolize_keys(hash)
|
111
|
+
deep_transform_keys(hash){ |key| key.to_sym rescue key }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Destructively convert all keys to symbols, as long as they respond
|
115
|
+
# to +to_sym+. This includes the keys from the root hash and from all
|
116
|
+
# nested hashes and arrays.
|
117
|
+
def deep_symbolize_keys!(hash)
|
118
|
+
deep_transform_keys!(hash){ |key| key.to_sym rescue key }
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
# support methods for deep transforming nested hashes and arrays
|
123
|
+
def _deep_transform_keys_in_object(object, &block)
|
124
|
+
if object.class <= ::Hash
|
125
|
+
object.each_with_object({}) do |(key, value), result|
|
126
|
+
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
|
127
|
+
end
|
128
|
+
elsif object.class <= ::Array
|
129
|
+
object.map {|e| _deep_transform_keys_in_object(e, &block) }
|
130
|
+
else
|
131
|
+
object
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def _deep_transform_keys_in_object!(object, &block)
|
136
|
+
if object.class <= ::Hash
|
137
|
+
object.keys.each do |key|
|
138
|
+
value = object.delete(key)
|
139
|
+
object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
|
140
|
+
end
|
141
|
+
object
|
142
|
+
elsif object.class <= ::Array
|
143
|
+
object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
|
144
|
+
else
|
145
|
+
object
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Fusu
|
2
|
+
module Hash
|
3
|
+
module ReverseMerge
|
4
|
+
# Merges the caller into +other_hash+. For example,
|
5
|
+
#
|
6
|
+
# options = Fusu::Hash.reverse_merge(options, {size: 25, velocity: 10})
|
7
|
+
#
|
8
|
+
# is equivalent to
|
9
|
+
#
|
10
|
+
# options = { size: 25, velocity: 10 }.merge(options)
|
11
|
+
#
|
12
|
+
# This is particularly useful for initializing an options hash
|
13
|
+
# with default values.
|
14
|
+
def reverse_merge(hash, other_hash)
|
15
|
+
other_hash.merge(hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Destructive +reverse_merge+.
|
19
|
+
def reverse_merge!(hash, other_hash)
|
20
|
+
# right wins if there is no left
|
21
|
+
hash.merge!( other_hash ){|key,left,right| left }
|
22
|
+
end
|
23
|
+
alias_method :reverse_update, :reverse_merge!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
require 'fusu/hash/keys'
|
2
|
+
require 'fusu/hash/reverse_merge'
|
3
|
+
|
4
|
+
module Fusu
|
5
|
+
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
|
6
|
+
# to be the same.
|
7
|
+
#
|
8
|
+
# rgb = Fusu::HashWithIndifferentAccess.new
|
9
|
+
#
|
10
|
+
# rgb[:black] = '#000000'
|
11
|
+
# rgb[:black] # => '#000000'
|
12
|
+
# rgb['black'] # => '#000000'
|
13
|
+
#
|
14
|
+
# rgb['white'] = '#FFFFFF'
|
15
|
+
# rgb[:white] # => '#FFFFFF'
|
16
|
+
# rgb['white'] # => '#FFFFFF'
|
17
|
+
#
|
18
|
+
# Internally symbols are mapped to strings when used as keys in the entire
|
19
|
+
# writing interface (calling <tt>[]=</tt>, <tt>merge</tt>, etc). This
|
20
|
+
# mapping belongs to the public interface. For example, given:
|
21
|
+
#
|
22
|
+
# hash = Fusu::HashWithIndifferentAccess.new(a: 1)
|
23
|
+
#
|
24
|
+
# You are guaranteed that the key is returned as a string:
|
25
|
+
#
|
26
|
+
# hash.keys # => ["a"]
|
27
|
+
#
|
28
|
+
# Technically other types of keys are accepted:
|
29
|
+
#
|
30
|
+
# hash = Fusu::HashWithIndifferentAccess.new(a: 1)
|
31
|
+
# hash[0] = 0
|
32
|
+
# hash # => {"a"=>1, 0=>0}
|
33
|
+
#
|
34
|
+
# but this class is intended for use cases where strings or symbols are the
|
35
|
+
# expected keys and it is convenient to understand both as the same. For
|
36
|
+
# example the +params+ hash in Ruby on Rails.
|
37
|
+
#
|
38
|
+
# Note that core extensions define <tt>Hash#with_indifferent_access</tt>:
|
39
|
+
#
|
40
|
+
# rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access
|
41
|
+
#
|
42
|
+
# which may be handy.
|
43
|
+
class HashWithIndifferentAccess < ::Hash
|
44
|
+
# Returns +true+ so that <tt>Array#extract_options!</tt> finds members of
|
45
|
+
# this class.
|
46
|
+
def extractable_options?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_indifferent_access
|
51
|
+
dup
|
52
|
+
end
|
53
|
+
|
54
|
+
def nested_under_indifferent_access
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize(constructor = {})
|
59
|
+
|
60
|
+
if constructor.respond_to?(:to_hash)
|
61
|
+
super()
|
62
|
+
update(constructor)
|
63
|
+
else
|
64
|
+
super(constructor)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def default(key = nil)
|
69
|
+
if key.is_a?(Symbol) && include?(key = key.to_s)
|
70
|
+
self[key]
|
71
|
+
else
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.new_from_hash_copying_default(hash)
|
77
|
+
hash = hash.to_hash
|
78
|
+
new(hash).tap do |new_hash|
|
79
|
+
new_hash.default = hash.default
|
80
|
+
new_hash.default_proc = hash.default_proc if hash.default_proc
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.[](*args)
|
85
|
+
new.merge!(::Hash[*args])
|
86
|
+
end
|
87
|
+
|
88
|
+
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
89
|
+
alias_method :regular_update, :update unless method_defined?(:regular_update)
|
90
|
+
|
91
|
+
# Assigns a new value to the hash:
|
92
|
+
#
|
93
|
+
# hash = Fusu::HashWithIndifferentAccess.new
|
94
|
+
# hash[:key] = 'value'
|
95
|
+
#
|
96
|
+
# This value can be later fetched using either +:key+ or +'key'+.
|
97
|
+
def []=(key, value)
|
98
|
+
regular_writer(convert_key(key), convert_value(value, for: :assignment))
|
99
|
+
end
|
100
|
+
|
101
|
+
alias_method :store, :[]=
|
102
|
+
|
103
|
+
# Updates the receiver in-place, merging in the hash passed as argument:
|
104
|
+
#
|
105
|
+
# hash_1 = Fusu::HashWithIndifferentAccess.new
|
106
|
+
# hash_1[:key] = 'value'
|
107
|
+
#
|
108
|
+
# hash_2 = Fusu::HashWithIndifferentAccess.new
|
109
|
+
# hash_2[:key] = 'New Value!'
|
110
|
+
#
|
111
|
+
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
|
112
|
+
#
|
113
|
+
# The argument can be either an
|
114
|
+
# <tt>Fusu::HashWithIndifferentAccess</tt> or a regular +Hash+.
|
115
|
+
# In either case the merge respects the semantics of indifferent access.
|
116
|
+
#
|
117
|
+
# If the argument is a regular hash with keys +:key+ and +"key"+ only one
|
118
|
+
# of the values end up in the receiver, but which one is unspecified.
|
119
|
+
#
|
120
|
+
# When given a block, the value for duplicated keys will be determined
|
121
|
+
# by the result of invoking the block with the duplicated key, the value
|
122
|
+
# in the receiver, and the value in +other_hash+. The rules for duplicated
|
123
|
+
# keys follow the semantics of indifferent access:
|
124
|
+
#
|
125
|
+
# hash_1[:key] = 10
|
126
|
+
# hash_2['key'] = 12
|
127
|
+
# hash_1.update(hash_2) { |key, old, new| old + new } # => {"key"=>22}
|
128
|
+
def update(other_hash)
|
129
|
+
if other_hash.is_a? HashWithIndifferentAccess
|
130
|
+
super(other_hash)
|
131
|
+
else
|
132
|
+
other_hash.to_hash.each_pair do |key, value|
|
133
|
+
if block_given? && key?(key)
|
134
|
+
value = yield(convert_key(key), self[key], value)
|
135
|
+
end
|
136
|
+
regular_writer(convert_key(key), convert_value(value))
|
137
|
+
end
|
138
|
+
self
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
alias_method :merge!, :update
|
143
|
+
|
144
|
+
# Checks the hash for a key matching the argument passed in:
|
145
|
+
#
|
146
|
+
# hash = Fusu::HashWithIndifferentAccess.new
|
147
|
+
# hash['key'] = 'value'
|
148
|
+
# hash.key?(:key) # => true
|
149
|
+
# hash.key?('key') # => true
|
150
|
+
def key?(key)
|
151
|
+
super(convert_key(key))
|
152
|
+
end
|
153
|
+
|
154
|
+
alias_method :include?, :key?
|
155
|
+
alias_method :has_key?, :key?
|
156
|
+
alias_method :member?, :key?
|
157
|
+
|
158
|
+
# Same as <tt>Hash#fetch</tt> where the key passed as argument can be
|
159
|
+
# either a string or a symbol:
|
160
|
+
#
|
161
|
+
# counters = Fusu::HashWithIndifferentAccess.new
|
162
|
+
# counters[:foo] = 1
|
163
|
+
#
|
164
|
+
# counters.fetch('foo') # => 1
|
165
|
+
# counters.fetch(:bar, 0) # => 0
|
166
|
+
# counters.fetch(:bar) { |key| 0 } # => 0
|
167
|
+
# counters.fetch(:zoo) # => KeyError: key not found: "zoo"
|
168
|
+
def fetch(key, *extras)
|
169
|
+
super(convert_key(key), *extras)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns an array of the values at the specified indices:
|
173
|
+
#
|
174
|
+
# hash = Fusu::HashWithIndifferentAccess.new
|
175
|
+
# hash[:a] = 'x'
|
176
|
+
# hash[:b] = 'y'
|
177
|
+
# hash.values_at('a', 'b') # => ["x", "y"]
|
178
|
+
def values_at(*indices)
|
179
|
+
indices.collect { |key| self[convert_key(key)] }
|
180
|
+
end
|
181
|
+
|
182
|
+
# Returns a shallow copy of the hash.
|
183
|
+
#
|
184
|
+
# hash = Fusu::HashWithIndifferentAccess.new({ a: { b: 'b' } })
|
185
|
+
# dup = hash.dup
|
186
|
+
# dup[:a][:c] = 'c'
|
187
|
+
#
|
188
|
+
# hash[:a][:c] # => nil
|
189
|
+
# dup[:a][:c] # => "c"
|
190
|
+
def dup
|
191
|
+
self.class.new(self).tap do |new_hash|
|
192
|
+
set_defaults(new_hash)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# This method has the same semantics of +update+, except it does not
|
197
|
+
# modify the receiver but rather returns a new hash with indifferent
|
198
|
+
# access with the result of the merge.
|
199
|
+
def merge(hash, &block)
|
200
|
+
self.dup.update(hash, &block)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Like +merge+ but the other way around: Merges the receiver into the
|
204
|
+
# argument and returns a new hash with indifferent access as result:
|
205
|
+
#
|
206
|
+
# hash = Fusu::HashWithIndifferentAccess.new
|
207
|
+
# hash['a'] = nil
|
208
|
+
# hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
|
209
|
+
def reverse_merge(other_hash)
|
210
|
+
super(self.class.new_from_hash_copying_default(other_hash))
|
211
|
+
end
|
212
|
+
|
213
|
+
# Same semantics as +reverse_merge+ but modifies the receiver in-place.
|
214
|
+
def reverse_merge!(other_hash)
|
215
|
+
replace(Fusu::Hash.reverse_merge(self, other_hash ))
|
216
|
+
end
|
217
|
+
|
218
|
+
# Replaces the contents of this hash with other_hash.
|
219
|
+
#
|
220
|
+
# h = { "a" => 100, "b" => 200 }
|
221
|
+
# h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
|
222
|
+
def replace(other_hash)
|
223
|
+
super(self.class.new_from_hash_copying_default(other_hash))
|
224
|
+
end
|
225
|
+
|
226
|
+
# Removes the specified key from the hash.
|
227
|
+
def delete(key)
|
228
|
+
super(convert_key(key))
|
229
|
+
end
|
230
|
+
|
231
|
+
def stringify_keys!; self end
|
232
|
+
def deep_stringify_keys!; self end
|
233
|
+
def stringify_keys; dup end
|
234
|
+
def deep_stringify_keys; dup end
|
235
|
+
def symbolize_keys; Fusu::Hash.symbolize_keys!(to_hash) end
|
236
|
+
def deep_symbolize_keys; Fusu::Hash.deep_symbolize_keys!(to_hash) end
|
237
|
+
def to_options!; self end
|
238
|
+
|
239
|
+
def select(*args, &block)
|
240
|
+
dup.tap { |hash| hash.select!(*args, &block) }
|
241
|
+
end
|
242
|
+
|
243
|
+
def reject(*args, &block)
|
244
|
+
dup.tap { |hash| hash.reject!(*args, &block) }
|
245
|
+
end
|
246
|
+
|
247
|
+
# Convert to a regular hash with string keys.
|
248
|
+
def to_hash
|
249
|
+
_new_hash = ::Hash.new
|
250
|
+
set_defaults(_new_hash)
|
251
|
+
|
252
|
+
each do |key, value|
|
253
|
+
_new_hash[key] = convert_value(value, for: :to_hash)
|
254
|
+
end
|
255
|
+
_new_hash
|
256
|
+
end
|
257
|
+
|
258
|
+
protected
|
259
|
+
def convert_key(key)
|
260
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
261
|
+
end
|
262
|
+
|
263
|
+
def convert_value(value, options = {})
|
264
|
+
if value.is_a? ::Hash
|
265
|
+
if options[:for] == :to_hash
|
266
|
+
value.to_hash
|
267
|
+
else
|
268
|
+
self.class.new(value)
|
269
|
+
end
|
270
|
+
elsif value.is_a?(::Array)
|
271
|
+
if options[:for] != :assignment || value.frozen?
|
272
|
+
value = value.dup
|
273
|
+
end
|
274
|
+
value.map! { |e| convert_value(e, options) }
|
275
|
+
else
|
276
|
+
value
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def set_defaults(target)
|
281
|
+
if default_proc
|
282
|
+
target.default_proc = default_proc.dup
|
283
|
+
else
|
284
|
+
target.default = default
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
data/lib/fusu/regexp.rb
ADDED
data/lib/fusu/string.rb
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Fusu
|
3
|
+
module String
|
4
|
+
module Methods
|
5
|
+
ACRONYM_REGEX = /(?=a)b/
|
6
|
+
# The Inflector transforms words from singular to plural, class names to table
|
7
|
+
# names, modularized class names to ones without, and class names to foreign
|
8
|
+
# keys. The default inflections for pluralization, singularization, and
|
9
|
+
# uncountable words are kept in inflections.rb.
|
10
|
+
#
|
11
|
+
# The Rails core team has stated patches for the inflections library will not
|
12
|
+
# be accepted in order to avoid breaking legacy applications which may be
|
13
|
+
# relying on errant inflections. If you discover an incorrect inflection and
|
14
|
+
# require it for your application or wish to define rules for languages other
|
15
|
+
# than English, please correct or add them yourself (explained below).
|
16
|
+
|
17
|
+
# Converts strings to UpperCamelCase.
|
18
|
+
# If the +uppercase_first_letter+ parameter is set to false, then produces
|
19
|
+
# lowerCamelCase.
|
20
|
+
#
|
21
|
+
# Also converts '/' to '::' which is useful for converting
|
22
|
+
# paths to namespaces.
|
23
|
+
#
|
24
|
+
# camelize('active_model') # => "ActiveModel"
|
25
|
+
# camelize('active_model', false) # => "activeModel"
|
26
|
+
# camelize('active_model/errors') # => "ActiveModel::Errors"
|
27
|
+
# camelize('active_model/errors', false) # => "activeModel::Errors"
|
28
|
+
#
|
29
|
+
# As a rule of thumb you can think of +camelize+ as the inverse of
|
30
|
+
# #underscore, though there are cases where that does not hold:
|
31
|
+
#
|
32
|
+
# camelize(underscore('SSLError')) # => "SslError"
|
33
|
+
def camelize(term, uppercase_first_letter = true)
|
34
|
+
string = term.to_s
|
35
|
+
if uppercase_first_letter
|
36
|
+
string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
|
37
|
+
else
|
38
|
+
string = string.sub(/^(?:#{ACRONYM_REGEX}(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
|
39
|
+
end
|
40
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
|
41
|
+
string.gsub!("/".freeze, "::".freeze)
|
42
|
+
string
|
43
|
+
end
|
44
|
+
|
45
|
+
# Makes an underscored, lowercase form from the expression in the string.
|
46
|
+
#
|
47
|
+
# Changes '::' to '/' to convert namespaces to paths.
|
48
|
+
#
|
49
|
+
# underscore('ActiveModel') # => "active_model"
|
50
|
+
# underscore('ActiveModel::Errors') # => "active_model/errors"
|
51
|
+
#
|
52
|
+
# As a rule of thumb you can think of +underscore+ as the inverse of
|
53
|
+
# #camelize, though there are cases where that does not hold:
|
54
|
+
#
|
55
|
+
# camelize(underscore('SSLError')) # => "SslError"
|
56
|
+
def underscore(camel_cased_word)
|
57
|
+
return camel_cased_word unless Fusu::Regexp.match?(/[A-Z-]|::/, camel_cased_word)
|
58
|
+
word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze)
|
59
|
+
word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{ACRONYM_REGEX})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }" }
|
60
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
|
61
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
|
62
|
+
word.tr!("-".freeze, "_".freeze)
|
63
|
+
word.downcase!
|
64
|
+
word
|
65
|
+
end
|
66
|
+
|
67
|
+
# Converts just the first character to uppercase.
|
68
|
+
#
|
69
|
+
# upcase_first('what a Lovely Day') # => "What a Lovely Day"
|
70
|
+
# upcase_first('w') # => "W"
|
71
|
+
# upcase_first('') # => ""
|
72
|
+
def upcase_first(string)
|
73
|
+
string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
|
74
|
+
end
|
75
|
+
|
76
|
+
# Tweaks an attribute name for display to end users.
|
77
|
+
#
|
78
|
+
# Specifically, performs these transformations:
|
79
|
+
#
|
80
|
+
# * Applies human inflection rules to the argument.
|
81
|
+
# * Deletes leading underscores, if any.
|
82
|
+
# * Removes a "_id" suffix if present.
|
83
|
+
# * Replaces underscores with spaces, if any.
|
84
|
+
# * Downcases all words except acronyms.
|
85
|
+
# * Capitalizes the first word.
|
86
|
+
# The capitalization of the first word can be turned off by setting the
|
87
|
+
# +:capitalize+ option to false (default is true).
|
88
|
+
#
|
89
|
+
# The trailing '_id' can be kept and capitalized by setting the
|
90
|
+
# optional parameter +keep_id_suffix+ to true (default is false).
|
91
|
+
#
|
92
|
+
# humanize('employee_salary') # => "Employee salary"
|
93
|
+
# humanize('author_id') # => "Author"
|
94
|
+
# humanize('author_id', capitalize: false) # => "author"
|
95
|
+
# humanize('_id') # => "Id"
|
96
|
+
# humanize('author_id', keep_id_suffix: true) # => "Author Id"
|
97
|
+
#
|
98
|
+
# If "SSL" was defined to be an acronym:
|
99
|
+
#
|
100
|
+
# humanize('ssl_error') # => "SSL error"
|
101
|
+
#
|
102
|
+
def humanize(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false)
|
103
|
+
result = lower_case_and_underscored_word.to_s.dup
|
104
|
+
|
105
|
+
# inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
|
106
|
+
|
107
|
+
result.sub!(/\A_+/, "".freeze)
|
108
|
+
unless keep_id_suffix
|
109
|
+
result.sub!(/_id\z/, "".freeze)
|
110
|
+
end
|
111
|
+
result.tr!("_".freeze, " ".freeze)
|
112
|
+
|
113
|
+
result.gsub!(/([a-z\d]*)/i) do |match|
|
114
|
+
"#{match.downcase}"
|
115
|
+
end
|
116
|
+
|
117
|
+
if capitalize
|
118
|
+
result.sub!(/\A\w/) { |match| match.upcase }
|
119
|
+
end
|
120
|
+
|
121
|
+
result
|
122
|
+
end
|
123
|
+
|
124
|
+
# Capitalizes all the words and replaces some characters in the string to
|
125
|
+
# create a nicer looking title. +titleize+ is meant for creating pretty
|
126
|
+
# output. It is not used in the Rails internals.
|
127
|
+
#
|
128
|
+
# The trailing '_id','Id'.. can be kept and capitalized by setting the
|
129
|
+
# optional parameter +keep_id_suffix+ to true.
|
130
|
+
# By default, this parameter is false.
|
131
|
+
#
|
132
|
+
# +titleize+ is also aliased as +titlecase+.
|
133
|
+
#
|
134
|
+
# titleize('man from the boondocks') # => "Man From The Boondocks"
|
135
|
+
# titleize('x-men: the last stand') # => "X Men: The Last Stand"
|
136
|
+
# titleize('TheManWithoutAPast') # => "The Man Without A Past"
|
137
|
+
# titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
|
138
|
+
# titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id"
|
139
|
+
def titleize(word, keep_id_suffix: false)
|
140
|
+
humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['’`])[a-z]/) do |match|
|
141
|
+
match.capitalize
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Replaces underscores with dashes in the string.
|
146
|
+
#
|
147
|
+
# dasherize('puni_puni') # => "puni-puni"
|
148
|
+
def dasherize(underscored_word)
|
149
|
+
underscored_word.tr("_".freeze, "-".freeze)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Removes the module part from the expression in the string.
|
153
|
+
#
|
154
|
+
# demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
|
155
|
+
# demodulize('Inflections') # => "Inflections"
|
156
|
+
# demodulize('::Inflections') # => "Inflections"
|
157
|
+
# demodulize('') # => ""
|
158
|
+
#
|
159
|
+
# See also #deconstantize.
|
160
|
+
def demodulize(path)
|
161
|
+
path = path.to_s
|
162
|
+
if i = path.rindex("::")
|
163
|
+
path[(i + 2)..-1]
|
164
|
+
else
|
165
|
+
path
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Removes the rightmost segment from the constant expression in the string.
|
170
|
+
#
|
171
|
+
# deconstantize('Net::HTTP') # => "Net"
|
172
|
+
# deconstantize('::Net::HTTP') # => "::Net"
|
173
|
+
# deconstantize('String') # => ""
|
174
|
+
# deconstantize('::String') # => ""
|
175
|
+
# deconstantize('') # => ""
|
176
|
+
#
|
177
|
+
# See also #demodulize.
|
178
|
+
def deconstantize(path)
|
179
|
+
path.to_s[0, path.rindex("::") || 0] # implementation based on the one in facets' Module#spacename
|
180
|
+
end
|
181
|
+
|
182
|
+
# Creates a foreign key name from a class name.
|
183
|
+
# +separate_class_name_and_id_with_underscore+ sets whether
|
184
|
+
# the method should put '_' between the name and 'id'.
|
185
|
+
#
|
186
|
+
# foreign_key('Message') # => "message_id"
|
187
|
+
# foreign_key('Message', false) # => "messageid"
|
188
|
+
# foreign_key('Admin::Post') # => "post_id"
|
189
|
+
def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
|
190
|
+
underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
|
191
|
+
end
|
192
|
+
|
193
|
+
# Tries to find a constant with the name specified in the argument string.
|
194
|
+
#
|
195
|
+
# constantize('Module') # => Module
|
196
|
+
# constantize('Foo::Bar') # => Foo::Bar
|
197
|
+
#
|
198
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
199
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
200
|
+
# account:
|
201
|
+
#
|
202
|
+
# C = 'outside'
|
203
|
+
# module M
|
204
|
+
# C = 'inside'
|
205
|
+
# C # => 'inside'
|
206
|
+
# constantize('C') # => 'outside', same as ::C
|
207
|
+
# end
|
208
|
+
#
|
209
|
+
# NameError is raised when the name is not in CamelCase or the constant is
|
210
|
+
# unknown.
|
211
|
+
def constantize(camel_cased_word)
|
212
|
+
names = camel_cased_word.split("::".freeze)
|
213
|
+
|
214
|
+
# Trigger a built-in NameError exception including the ill-formed constant in the message.
|
215
|
+
Object.const_get(camel_cased_word) if names.empty?
|
216
|
+
|
217
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
218
|
+
names.shift if names.size > 1 && names.first.empty?
|
219
|
+
|
220
|
+
names.inject(Object) do |constant, name|
|
221
|
+
if constant == Object
|
222
|
+
constant.const_get(name)
|
223
|
+
else
|
224
|
+
candidate = constant.const_get(name)
|
225
|
+
next candidate if constant.const_defined?(name, false)
|
226
|
+
next candidate unless Object.const_defined?(name)
|
227
|
+
|
228
|
+
# Go down the ancestors to check if it is owned directly. The check
|
229
|
+
# stops when we reach Object or the end of ancestors tree.
|
230
|
+
constant = constant.ancestors.inject(constant) do |const, ancestor|
|
231
|
+
break const if ancestor == Object
|
232
|
+
break ancestor if ancestor.const_defined?(name, false)
|
233
|
+
const
|
234
|
+
end
|
235
|
+
|
236
|
+
# owner is in Object, so raise
|
237
|
+
constant.const_get(name, false)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Tries to find a constant with the name specified in the argument string.
|
243
|
+
#
|
244
|
+
# safe_constantize('Module') # => Module
|
245
|
+
# safe_constantize('Foo::Bar') # => Foo::Bar
|
246
|
+
#
|
247
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
248
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
249
|
+
# account:
|
250
|
+
#
|
251
|
+
# C = 'outside'
|
252
|
+
# module M
|
253
|
+
# C = 'inside'
|
254
|
+
# C # => 'inside'
|
255
|
+
# safe_constantize('C') # => 'outside', same as ::C
|
256
|
+
# end
|
257
|
+
#
|
258
|
+
# +nil+ is returned when the name is not in CamelCase or the constant (or
|
259
|
+
# part of it) is unknown.
|
260
|
+
#
|
261
|
+
# safe_constantize('blargle') # => nil
|
262
|
+
# safe_constantize('UnknownModule') # => nil
|
263
|
+
# safe_constantize('UnknownModule::Foo::Bar') # => nil
|
264
|
+
def safe_constantize(camel_cased_word)
|
265
|
+
constantize(camel_cased_word)
|
266
|
+
rescue NameError => e
|
267
|
+
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
|
268
|
+
e.name.to_s == camel_cased_word.to_s)
|
269
|
+
rescue ArgumentError => e
|
270
|
+
raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Returns the suffix that should be added to a number to denote the position
|
274
|
+
# in an ordered sequence such as 1st, 2nd, 3rd, 4th.
|
275
|
+
#
|
276
|
+
# ordinal(1) # => "st"
|
277
|
+
# ordinal(2) # => "nd"
|
278
|
+
# ordinal(1002) # => "nd"
|
279
|
+
# ordinal(1003) # => "rd"
|
280
|
+
# ordinal(-11) # => "th"
|
281
|
+
# ordinal(-1021) # => "st"
|
282
|
+
def ordinal(number)
|
283
|
+
abs_number = number.to_i.abs
|
284
|
+
|
285
|
+
if (11..13).include?(abs_number % 100)
|
286
|
+
"th"
|
287
|
+
else
|
288
|
+
case abs_number % 10
|
289
|
+
when 1; "st"
|
290
|
+
when 2; "nd"
|
291
|
+
when 3; "rd"
|
292
|
+
else "th"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Turns a number into an ordinal string used to denote the position in an
|
298
|
+
# ordered sequence such as 1st, 2nd, 3rd, 4th.
|
299
|
+
#
|
300
|
+
# ordinalize(1) # => "1st"
|
301
|
+
# ordinalize(2) # => "2nd"
|
302
|
+
# ordinalize(1002) # => "1002nd"
|
303
|
+
# ordinalize(1003) # => "1003rd"
|
304
|
+
# ordinalize(-11) # => "-11th"
|
305
|
+
# ordinalize(-1021) # => "-1021st"
|
306
|
+
def ordinalize(number)
|
307
|
+
"#{number}#{ordinal(number)}"
|
308
|
+
end
|
309
|
+
|
310
|
+
private
|
311
|
+
|
312
|
+
# Mounts a regular expression, returned as a string to ease interpolation,
|
313
|
+
# that will match part by part the given constant.
|
314
|
+
#
|
315
|
+
# const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
|
316
|
+
# const_regexp("::") # => "::"
|
317
|
+
def const_regexp(camel_cased_word)
|
318
|
+
parts = camel_cased_word.split("::".freeze)
|
319
|
+
|
320
|
+
return Regexp.escape(camel_cased_word) if parts.blank?
|
321
|
+
|
322
|
+
last = parts.pop
|
323
|
+
|
324
|
+
parts.reverse.inject(last) do |acc, part|
|
325
|
+
part.empty? ? acc : "#{part}(::#{acc})?"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Applies inflection rules for +singularize+ and +pluralize+.
|
330
|
+
#
|
331
|
+
# If passed an optional +locale+ parameter, the uncountables will be
|
332
|
+
# found for that locale.
|
333
|
+
#
|
334
|
+
# apply_inflections('post', inflections.plurals, :en) # => "posts"
|
335
|
+
# apply_inflections('posts', inflections.singulars, :en) # => "post"
|
336
|
+
# def apply_inflections(word, rules, locale = :en)
|
337
|
+
# result = word.to_s.dup
|
338
|
+
#
|
339
|
+
# if word.empty? || inflections(locale).uncountables.uncountable?(result)
|
340
|
+
# result
|
341
|
+
# else
|
342
|
+
# rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
|
343
|
+
# result
|
344
|
+
# end
|
345
|
+
# end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
data/lib/fusu/try.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Fusu
|
2
|
+
module Try
|
3
|
+
def try(object, *a, &b)
|
4
|
+
return nil if object === nil
|
5
|
+
try!(object, *a, &b) if a.empty? || object.respond_to?(a.first)
|
6
|
+
end
|
7
|
+
|
8
|
+
def try!(object, *a, &b)
|
9
|
+
if a.empty? && block_given?
|
10
|
+
if b.arity == 0
|
11
|
+
object.instance_eval(&b)
|
12
|
+
else
|
13
|
+
yield object
|
14
|
+
end
|
15
|
+
else
|
16
|
+
object.public_send(*a, &b)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/fusu/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fusu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Artur Panyach
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -73,6 +73,17 @@ files:
|
|
73
73
|
- lib/fusu.rb
|
74
74
|
- lib/fusu/array.rb
|
75
75
|
- lib/fusu/blank.rb
|
76
|
+
- lib/fusu/concern.rb
|
77
|
+
- lib/fusu/hash.rb
|
78
|
+
- lib/fusu/hash/deep_merge.rb
|
79
|
+
- lib/fusu/hash/except.rb
|
80
|
+
- lib/fusu/hash/keys.rb
|
81
|
+
- lib/fusu/hash/reverse_merge.rb
|
82
|
+
- lib/fusu/hash_with_indifferent_access.rb
|
83
|
+
- lib/fusu/regexp.rb
|
84
|
+
- lib/fusu/string.rb
|
85
|
+
- lib/fusu/string/methods.rb
|
86
|
+
- lib/fusu/try.rb
|
76
87
|
- lib/fusu/version.rb
|
77
88
|
homepage: https://github.com/arturictus/fusu
|
78
89
|
licenses:
|