sigterm_extensions 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +6 -0
- data/LICENSE.md +0 -0
- data/README.md +0 -0
- data/bin/ctxirb +156 -0
- data/lib/git.rb +166 -0
- data/lib/git/LICENSE +21 -0
- data/lib/git/author.rb +14 -0
- data/lib/git/base.rb +551 -0
- data/lib/git/base/factory.rb +75 -0
- data/lib/git/branch.rb +126 -0
- data/lib/git/branches.rb +71 -0
- data/lib/git/config.rb +22 -0
- data/lib/git/diff.rb +159 -0
- data/lib/git/index.rb +5 -0
- data/lib/git/lib.rb +1041 -0
- data/lib/git/log.rb +128 -0
- data/lib/git/object.rb +312 -0
- data/lib/git/path.rb +31 -0
- data/lib/git/remote.rb +36 -0
- data/lib/git/repository.rb +6 -0
- data/lib/git/stash.rb +27 -0
- data/lib/git/stashes.rb +55 -0
- data/lib/git/status.rb +199 -0
- data/lib/git/version.rb +5 -0
- data/lib/git/working_directory.rb +4 -0
- data/lib/sigterm_extensions.rb +75 -0
- data/lib/sigterm_extensions/all.rb +12 -0
- data/lib/sigterm_extensions/backtrace_cleaner.rb +129 -0
- data/lib/sigterm_extensions/callbacks.rb +847 -0
- data/lib/sigterm_extensions/concern.rb +169 -0
- data/lib/sigterm_extensions/configurable.rb +38 -0
- data/lib/sigterm_extensions/core_ext.rb +4 -0
- data/lib/sigterm_extensions/core_ext/array.rb +3 -0
- data/lib/sigterm_extensions/core_ext/array/extract.rb +19 -0
- data/lib/sigterm_extensions/core_ext/array/extract_options.rb +29 -0
- data/lib/sigterm_extensions/core_ext/class.rb +3 -0
- data/lib/sigterm_extensions/core_ext/class/attribute.rb +139 -0
- data/lib/sigterm_extensions/core_ext/class/attribute_accessors.rb +4 -0
- data/lib/sigterm_extensions/core_ext/class/subclasses.rb +52 -0
- data/lib/sigterm_extensions/core_ext/custom.rb +12 -0
- data/lib/sigterm_extensions/core_ext/digest.rb +3 -0
- data/lib/sigterm_extensions/core_ext/digest/uuid.rb +51 -0
- data/lib/sigterm_extensions/core_ext/enumerable.rb +232 -0
- data/lib/sigterm_extensions/core_ext/file.rb +3 -0
- data/lib/sigterm_extensions/core_ext/file/atomic.rb +68 -0
- data/lib/sigterm_extensions/core_ext/hash.rb +3 -0
- data/lib/sigterm_extensions/core_ext/hash/deep_merge.rb +41 -0
- data/lib/sigterm_extensions/core_ext/hash/deep_transform_values.rb +44 -0
- data/lib/sigterm_extensions/core_ext/hash/except.rb +22 -0
- data/lib/sigterm_extensions/core_ext/hash/keys.rb +141 -0
- data/lib/sigterm_extensions/core_ext/hash/reverse_merge.rb +23 -0
- data/lib/sigterm_extensions/core_ext/hash/slice.rb +24 -0
- data/lib/sigterm_extensions/core_ext/kernel.rb +3 -0
- data/lib/sigterm_extensions/core_ext/kernel/concern.rb +12 -0
- data/lib/sigterm_extensions/core_ext/kernel/reporting.rb +43 -0
- data/lib/sigterm_extensions/core_ext/kernel/singleton_class.rb +6 -0
- data/lib/sigterm_extensions/core_ext/load_error.rb +7 -0
- data/lib/sigterm_extensions/core_ext/module.rb +3 -0
- data/lib/sigterm_extensions/core_ext/module/aliasing.rb +29 -0
- data/lib/sigterm_extensions/core_ext/module/anonymous.rb +28 -0
- data/lib/sigterm_extensions/core_ext/module/attr_internal.rb +36 -0
- data/lib/sigterm_extensions/core_ext/module/attribute_accessors.rb +208 -0
- data/lib/sigterm_extensions/core_ext/module/attribute_accessors_per_thread.rb +146 -0
- data/lib/sigterm_extensions/core_ext/module/concerning.rb +132 -0
- data/lib/sigterm_extensions/core_ext/module/delegation.rb +319 -0
- data/lib/sigterm_extensions/core_ext/module/redefine_method.rb +38 -0
- data/lib/sigterm_extensions/core_ext/module/remove_method.rb +15 -0
- data/lib/sigterm_extensions/core_ext/name_error.rb +36 -0
- data/lib/sigterm_extensions/core_ext/object.rb +3 -0
- data/lib/sigterm_extensions/core_ext/object/blank.rb +153 -0
- data/lib/sigterm_extensions/core_ext/object/colors.rb +39 -0
- data/lib/sigterm_extensions/core_ext/object/duplicable.rb +47 -0
- data/lib/sigterm_extensions/core_ext/object/inclusion.rb +27 -0
- data/lib/sigterm_extensions/core_ext/object/instance_variables.rb +28 -0
- data/lib/sigterm_extensions/core_ext/object/methods.rb +61 -0
- data/lib/sigterm_extensions/core_ext/object/with_options.rb +80 -0
- data/lib/sigterm_extensions/core_ext/range.rb +3 -0
- data/lib/sigterm_extensions/core_ext/range/compare_range.rb +74 -0
- data/lib/sigterm_extensions/core_ext/range/conversions.rb +39 -0
- data/lib/sigterm_extensions/core_ext/range/overlaps.rb +8 -0
- data/lib/sigterm_extensions/core_ext/securerandom.rb +43 -0
- data/lib/sigterm_extensions/core_ext/string.rb +3 -0
- data/lib/sigterm_extensions/core_ext/string/access.rb +93 -0
- data/lib/sigterm_extensions/core_ext/string/filters.rb +143 -0
- data/lib/sigterm_extensions/core_ext/string/starts_ends_with.rb +4 -0
- data/lib/sigterm_extensions/core_ext/string/strip.rb +25 -0
- data/lib/sigterm_extensions/core_ext/tryable.rb +132 -0
- data/lib/sigterm_extensions/descendants_tracker.rb +108 -0
- data/lib/sigterm_extensions/gem_methods.rb +47 -0
- data/lib/sigterm_extensions/hash_binding.rb +16 -0
- data/lib/sigterm_extensions/inflector.rb +339 -0
- data/lib/sigterm_extensions/inflector/acronyms.rb +42 -0
- data/lib/sigterm_extensions/inflector/inflections.rb +249 -0
- data/lib/sigterm_extensions/inflector/inflections/defaults.rb +117 -0
- data/lib/sigterm_extensions/inflector/rules.rb +37 -0
- data/lib/sigterm_extensions/inflector/version.rb +8 -0
- data/lib/sigterm_extensions/interactive_editor.rb +120 -0
- data/lib/sigterm_extensions/lazy.rb +34 -0
- data/lib/sigterm_extensions/lazy_load_hooks.rb +79 -0
- data/lib/sigterm_extensions/option_merger.rb +32 -0
- data/lib/sigterm_extensions/ordered_hash.rb +48 -0
- data/lib/sigterm_extensions/ordered_options.rb +83 -0
- data/lib/sigterm_extensions/paths.rb +235 -0
- data/lib/sigterm_extensions/per_thread_registry.rb +58 -0
- data/lib/sigterm_extensions/proxy_object.rb +14 -0
- data/lib/sigterm_extensions/staging/boot.rb +31 -0
- data/lib/sigterm_extensions/staging/boot/bundler_patch.rb +24 -0
- data/lib/sigterm_extensions/staging/boot/command.rb +26 -0
- data/lib/sigterm_extensions/staging/boot/gemfile_next_auto_sync.rb +79 -0
- data/lib/sigterm_extensions/version.rb +4 -0
- data/lib/sigterm_extensions/wrappable.rb +16 -0
- data/sigterm_extensions.gemspec +42 -0
- data/templates/dotpryrc.rb.erb +124 -0
- metadata +315 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
require "sigterm_extensions/option_merger"
|
2
|
+
|
3
|
+
class Object
|
4
|
+
# An elegant way to factor duplication out of options passed to a series of
|
5
|
+
# method calls. Each method called in the block, with the block variable as
|
6
|
+
# the receiver, will have its options merged with the default +options+ hash
|
7
|
+
# provided. Each method called on the block variable must take an options
|
8
|
+
# hash as its final argument.
|
9
|
+
#
|
10
|
+
# Without <tt>with_options</tt>, this code contains duplication:
|
11
|
+
#
|
12
|
+
# class Account < ActiveRecord::Base
|
13
|
+
# has_many :customers, dependent: :destroy
|
14
|
+
# has_many :products, dependent: :destroy
|
15
|
+
# has_many :invoices, dependent: :destroy
|
16
|
+
# has_many :expenses, dependent: :destroy
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Using <tt>with_options</tt>, we can remove the duplication:
|
20
|
+
#
|
21
|
+
# class Account < ActiveRecord::Base
|
22
|
+
# with_options dependent: :destroy do |assoc|
|
23
|
+
# assoc.has_many :customers
|
24
|
+
# assoc.has_many :products
|
25
|
+
# assoc.has_many :invoices
|
26
|
+
# assoc.has_many :expenses
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# It can also be used with an explicit receiver:
|
31
|
+
#
|
32
|
+
# I18n.with_options locale: user.locale, scope: 'newsletter' do |i18n|
|
33
|
+
# subject i18n.t :subject
|
34
|
+
# body i18n.t :body, user_name: user.name
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# When you don't pass an explicit receiver, it executes the whole block
|
38
|
+
# in merging options context:
|
39
|
+
#
|
40
|
+
# class Account < ActiveRecord::Base
|
41
|
+
# with_options dependent: :destroy do
|
42
|
+
# has_many :customers
|
43
|
+
# has_many :products
|
44
|
+
# has_many :invoices
|
45
|
+
# has_many :expenses
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# <tt>with_options</tt> can also be nested since the call is forwarded to its receiver.
|
50
|
+
#
|
51
|
+
# NOTE: Each nesting level will merge inherited defaults in addition to their own.
|
52
|
+
#
|
53
|
+
# class Post < ActiveRecord::Base
|
54
|
+
# with_options if: :persisted?, length: { minimum: 50 } do
|
55
|
+
# validates :content, if: -> { content.present? }
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# The code is equivalent to:
|
60
|
+
#
|
61
|
+
# validates :content, length: { minimum: 50 }, if: -> { content.present? }
|
62
|
+
#
|
63
|
+
# Hence the inherited default for +if+ key is ignored.
|
64
|
+
#
|
65
|
+
# NOTE: You cannot call class methods implicitly inside of with_options.
|
66
|
+
# You can access these methods using the class name instead:
|
67
|
+
#
|
68
|
+
# class Phone < ActiveRecord::Base
|
69
|
+
# enum phone_number_type: { home: 0, office: 1, mobile: 2 }
|
70
|
+
#
|
71
|
+
# with_options presence: true do
|
72
|
+
# validates :phone_number_type, inclusion: { in: Phone.phone_number_types.keys }
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
def with_options(options, &block)
|
77
|
+
option_merger = SigtermExtensions::OptionMerger.new(self, options)
|
78
|
+
block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module SigtermExtensions
|
2
|
+
module CompareWithRange
|
3
|
+
# Extends the default Range#=== to support range comparisons.
|
4
|
+
# (1..5) === (1..5) # => true
|
5
|
+
# (1..5) === (2..3) # => true
|
6
|
+
# (1..5) === (1...6) # => true
|
7
|
+
# (1..5) === (2..6) # => false
|
8
|
+
#
|
9
|
+
# The native Range#=== behavior is untouched.
|
10
|
+
# ('a'..'f') === ('c') # => true
|
11
|
+
# (5..9) === (11) # => false
|
12
|
+
#
|
13
|
+
# The given range must be fully bounded, with both start and end.
|
14
|
+
def ===(value)
|
15
|
+
if value.is_a?(::Range)
|
16
|
+
# 1...10 includes 1..9 but it does not include 1..10.
|
17
|
+
# 1..10 includes 1...11 but it does not include 1...12.
|
18
|
+
operator = exclude_end? && !value.exclude_end? ? :< : :<=
|
19
|
+
value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
|
20
|
+
super(value.first) && (self.end.nil? || value_max.send(operator, last))
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Extends the default Range#include? to support range comparisons.
|
27
|
+
# (1..5).include?(1..5) # => true
|
28
|
+
# (1..5).include?(2..3) # => true
|
29
|
+
# (1..5).include?(1...6) # => true
|
30
|
+
# (1..5).include?(2..6) # => false
|
31
|
+
#
|
32
|
+
# The native Range#include? behavior is untouched.
|
33
|
+
# ('a'..'f').include?('c') # => true
|
34
|
+
# (5..9).include?(11) # => false
|
35
|
+
#
|
36
|
+
# The given range must be fully bounded, with both start and end.
|
37
|
+
def include?(value)
|
38
|
+
if value.is_a?(::Range)
|
39
|
+
# 1...10 includes 1..9 but it does not include 1..10.
|
40
|
+
# 1..10 includes 1...11 but it does not include 1...12.
|
41
|
+
operator = exclude_end? && !value.exclude_end? ? :< : :<=
|
42
|
+
value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
|
43
|
+
super(value.first) && (self.end.nil? || value_max.send(operator, last))
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Extends the default Range#cover? to support range comparisons.
|
50
|
+
# (1..5).cover?(1..5) # => true
|
51
|
+
# (1..5).cover?(2..3) # => true
|
52
|
+
# (1..5).cover?(1...6) # => true
|
53
|
+
# (1..5).cover?(2..6) # => false
|
54
|
+
#
|
55
|
+
# The native Range#cover? behavior is untouched.
|
56
|
+
# ('a'..'f').cover?('c') # => true
|
57
|
+
# (5..9).cover?(11) # => false
|
58
|
+
#
|
59
|
+
# The given range must be fully bounded, with both start and end.
|
60
|
+
def cover?(value)
|
61
|
+
if value.is_a?(::Range)
|
62
|
+
# 1...10 covers 1..9 but it does not cover 1..10.
|
63
|
+
# 1..10 covers 1...11 but it does not cover 1...12.
|
64
|
+
operator = exclude_end? && !value.exclude_end? ? :< : :<=
|
65
|
+
value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
|
66
|
+
super(value.first) && (self.end.nil? || value_max.send(operator, last))
|
67
|
+
else
|
68
|
+
super
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Range.prepend(SigtermExtensions::CompareWithRange)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SigtermExtensions
|
2
|
+
module RangeWithFormat
|
3
|
+
RANGE_FORMATS = {
|
4
|
+
db: -> (start, stop) do
|
5
|
+
case start
|
6
|
+
when String then "BETWEEN '#{start}' AND '#{stop}'"
|
7
|
+
else
|
8
|
+
"BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
}
|
12
|
+
|
13
|
+
# Convert range to a formatted string. See RANGE_FORMATS for predefined formats.
|
14
|
+
#
|
15
|
+
# range = (1..100) # => 1..100
|
16
|
+
#
|
17
|
+
# range.to_s # => "1..100"
|
18
|
+
# range.to_s(:db) # => "BETWEEN '1' AND '100'"
|
19
|
+
#
|
20
|
+
# == Adding your own range formats to to_s
|
21
|
+
# You can add your own formats to the Range::RANGE_FORMATS hash.
|
22
|
+
# Use the format name as the hash key and a Proc instance.
|
23
|
+
#
|
24
|
+
# # config/initializers/range_formats.rb
|
25
|
+
# Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" }
|
26
|
+
def to_s(format = :default)
|
27
|
+
if formatter = RANGE_FORMATS[format]
|
28
|
+
formatter.call(first, last)
|
29
|
+
else
|
30
|
+
super()
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
alias_method :to_default_s, :to_s
|
35
|
+
alias_method :to_formatted_s, :to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Range.prepend(SigtermExtensions::RangeWithFormat)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
module SecureRandom
|
4
|
+
BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"]
|
5
|
+
BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a
|
6
|
+
|
7
|
+
# SecureRandom.base58 generates a random base58 string.
|
8
|
+
#
|
9
|
+
# The argument _n_ specifies the length of the random string to be generated.
|
10
|
+
#
|
11
|
+
# If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
|
12
|
+
#
|
13
|
+
# The result may contain alphanumeric characters except 0, O, I and l.
|
14
|
+
#
|
15
|
+
# p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
|
16
|
+
# p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
|
17
|
+
def self.base58(n = 16)
|
18
|
+
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
|
19
|
+
idx = byte % 64
|
20
|
+
idx = SecureRandom.random_number(58) if idx >= 58
|
21
|
+
BASE58_ALPHABET[idx]
|
22
|
+
end.join
|
23
|
+
end
|
24
|
+
|
25
|
+
# SecureRandom.base36 generates a random base36 string in lowercase.
|
26
|
+
#
|
27
|
+
# The argument _n_ specifies the length of the random string to be generated.
|
28
|
+
#
|
29
|
+
# If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
|
30
|
+
# This method can be used over +base58+ if a deterministic case key is necessary.
|
31
|
+
#
|
32
|
+
# The result will contain alphanumeric characters in lowercase.
|
33
|
+
#
|
34
|
+
# p SecureRandom.base36 # => "4kugl2pdqmscqtje"
|
35
|
+
# p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7"
|
36
|
+
def self.base36(n = 16)
|
37
|
+
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
|
38
|
+
idx = byte % 64
|
39
|
+
idx = SecureRandom.random_number(36) if idx >= 36
|
40
|
+
BASE36_ALPHABET[idx]
|
41
|
+
end.join
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class String
|
2
|
+
# If you pass a single integer, returns a substring of one character at that
|
3
|
+
# position. The first character of the string is at position 0, the next at
|
4
|
+
# position 1, and so on. If a range is supplied, a substring containing
|
5
|
+
# characters at offsets given by the range is returned. In both cases, if an
|
6
|
+
# offset is negative, it is counted from the end of the string. Returns +nil+
|
7
|
+
# if the initial offset falls outside the string. Returns an empty string if
|
8
|
+
# the beginning of the range is greater than the end of the string.
|
9
|
+
#
|
10
|
+
# str = "hello"
|
11
|
+
# str.at(0) # => "h"
|
12
|
+
# str.at(1..3) # => "ell"
|
13
|
+
# str.at(-2) # => "l"
|
14
|
+
# str.at(-2..-1) # => "lo"
|
15
|
+
# str.at(5) # => nil
|
16
|
+
# str.at(5..-1) # => ""
|
17
|
+
#
|
18
|
+
# If a Regexp is given, the matching portion of the string is returned.
|
19
|
+
# If a String is given, that given string is returned if it occurs in
|
20
|
+
# the string. In both cases, +nil+ is returned if there is no match.
|
21
|
+
#
|
22
|
+
# str = "hello"
|
23
|
+
# str.at(/lo/) # => "lo"
|
24
|
+
# str.at(/ol/) # => nil
|
25
|
+
# str.at("lo") # => "lo"
|
26
|
+
# str.at("ol") # => nil
|
27
|
+
def at(position)
|
28
|
+
self[position]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a substring from the given position to the end of the string.
|
32
|
+
# If the position is negative, it is counted from the end of the string.
|
33
|
+
#
|
34
|
+
# str = "hello"
|
35
|
+
# str.from(0) # => "hello"
|
36
|
+
# str.from(3) # => "lo"
|
37
|
+
# str.from(-2) # => "lo"
|
38
|
+
#
|
39
|
+
# You can mix it with +to+ method and do fun things like:
|
40
|
+
#
|
41
|
+
# str = "hello"
|
42
|
+
# str.from(0).to(-1) # => "hello"
|
43
|
+
# str.from(1).to(-2) # => "ell"
|
44
|
+
def from(position)
|
45
|
+
self[position, length]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns a substring from the beginning of the string to the given position.
|
49
|
+
# If the position is negative, it is counted from the end of the string.
|
50
|
+
#
|
51
|
+
# str = "hello"
|
52
|
+
# str.to(0) # => "h"
|
53
|
+
# str.to(3) # => "hell"
|
54
|
+
# str.to(-2) # => "hell"
|
55
|
+
#
|
56
|
+
# You can mix it with +from+ method and do fun things like:
|
57
|
+
#
|
58
|
+
# str = "hello"
|
59
|
+
# str.from(0).to(-1) # => "hello"
|
60
|
+
# str.from(1).to(-2) # => "ell"
|
61
|
+
def to(position)
|
62
|
+
position += size if position < 0
|
63
|
+
self[0, position + 1] || +""
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the first character. If a limit is supplied, returns a substring
|
67
|
+
# from the beginning of the string until it reaches the limit value. If the
|
68
|
+
# given limit is greater than or equal to the string length, returns a copy of self.
|
69
|
+
#
|
70
|
+
# str = "hello"
|
71
|
+
# str.first # => "h"
|
72
|
+
# str.first(1) # => "h"
|
73
|
+
# str.first(2) # => "he"
|
74
|
+
# str.first(0) # => ""
|
75
|
+
# str.first(6) # => "hello"
|
76
|
+
def first(limit = 1)
|
77
|
+
self[0, limit] || raise(ArgumentError, "negative limit")
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the last character of the string. If a limit is supplied, returns a substring
|
81
|
+
# from the end of the string until it reaches the limit value (counting backwards). If
|
82
|
+
# the given limit is greater than or equal to the string length, returns a copy of self.
|
83
|
+
#
|
84
|
+
# str = "hello"
|
85
|
+
# str.last # => "o"
|
86
|
+
# str.last(1) # => "o"
|
87
|
+
# str.last(2) # => "lo"
|
88
|
+
# str.last(0) # => ""
|
89
|
+
# str.last(6) # => "hello"
|
90
|
+
def last(limit = 1)
|
91
|
+
self[[length - limit, 0].max, limit] || raise(ArgumentError, "negative limit")
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
class String
|
2
|
+
# Returns the string, first removing all whitespace on both ends of
|
3
|
+
# the string, and then changing remaining consecutive whitespace
|
4
|
+
# groups into one space each.
|
5
|
+
#
|
6
|
+
# Note that it handles both ASCII and Unicode whitespace.
|
7
|
+
#
|
8
|
+
# %{ Multi-line
|
9
|
+
# string }.squish # => "Multi-line string"
|
10
|
+
# " foo bar \n \t boo".squish # => "foo bar boo"
|
11
|
+
def squish
|
12
|
+
dup.squish!
|
13
|
+
end
|
14
|
+
|
15
|
+
# Performs a destructive squish. See String#squish.
|
16
|
+
# str = " foo bar \n \t boo"
|
17
|
+
# str.squish! # => "foo bar boo"
|
18
|
+
# str # => "foo bar boo"
|
19
|
+
def squish!
|
20
|
+
gsub!(/[[:space:]]+/, " ")
|
21
|
+
strip!
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a new string with all occurrences of the patterns removed.
|
26
|
+
# str = "foo bar test"
|
27
|
+
# str.remove(" test") # => "foo bar"
|
28
|
+
# str.remove(" test", /bar/) # => "foo "
|
29
|
+
# str # => "foo bar test"
|
30
|
+
def remove(*patterns)
|
31
|
+
dup.remove!(*patterns)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Alters the string by removing all occurrences of the patterns.
|
35
|
+
# str = "foo bar test"
|
36
|
+
# str.remove!(" test", /bar/) # => "foo "
|
37
|
+
# str # => "foo "
|
38
|
+
def remove!(*patterns)
|
39
|
+
patterns.each do |pattern|
|
40
|
+
gsub! pattern, ""
|
41
|
+
end
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
|
47
|
+
#
|
48
|
+
# 'Once upon a time in a world far far away'.truncate(27)
|
49
|
+
# # => "Once upon a time in a wo..."
|
50
|
+
#
|
51
|
+
# Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
|
52
|
+
#
|
53
|
+
# 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
|
54
|
+
# # => "Once upon a time in a..."
|
55
|
+
#
|
56
|
+
# 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
|
57
|
+
# # => "Once upon a time in a..."
|
58
|
+
#
|
59
|
+
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
|
60
|
+
# for a total length not exceeding <tt>length</tt>:
|
61
|
+
#
|
62
|
+
# 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
|
63
|
+
# # => "And they f... (continued)"
|
64
|
+
def truncate(truncate_at, options = {})
|
65
|
+
return dup unless length > truncate_at
|
66
|
+
|
67
|
+
omission = options[:omission] || "..."
|
68
|
+
length_with_room_for_omission = truncate_at - omission.length
|
69
|
+
stop = \
|
70
|
+
if options[:separator]
|
71
|
+
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
|
72
|
+
else
|
73
|
+
length_with_room_for_omission
|
74
|
+
end
|
75
|
+
|
76
|
+
+"#{self[0, stop]}#{omission}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Truncates +text+ to at most <tt>bytesize</tt> bytes in length without
|
80
|
+
# breaking string encoding by splitting multibyte characters or breaking
|
81
|
+
# grapheme clusters ("perceptual characters") by truncating at combining
|
82
|
+
# characters.
|
83
|
+
#
|
84
|
+
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
|
85
|
+
# => 20
|
86
|
+
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
|
87
|
+
# => 80
|
88
|
+
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
|
89
|
+
# => "🔪🔪🔪🔪…"
|
90
|
+
#
|
91
|
+
# The truncated text ends with the <tt>:omission</tt> string, defaulting
|
92
|
+
# to "…", for a total length not exceeding <tt>bytesize</tt>.
|
93
|
+
def truncate_bytes(truncate_at, omission: "…")
|
94
|
+
omission ||= ""
|
95
|
+
|
96
|
+
case
|
97
|
+
when bytesize <= truncate_at
|
98
|
+
dup
|
99
|
+
when omission.bytesize > truncate_at
|
100
|
+
raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
|
101
|
+
when omission.bytesize == truncate_at
|
102
|
+
omission.dup
|
103
|
+
else
|
104
|
+
self.class.new.tap do |cut|
|
105
|
+
cut_at = truncate_at - omission.bytesize
|
106
|
+
|
107
|
+
scan(/\X/) do |grapheme|
|
108
|
+
if cut.bytesize + grapheme.bytesize <= cut_at
|
109
|
+
cut << grapheme
|
110
|
+
else
|
111
|
+
break
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
cut << omission
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
|
121
|
+
#
|
122
|
+
# 'Once upon a time in a world far far away'.truncate_words(4)
|
123
|
+
# # => "Once upon a time..."
|
124
|
+
#
|
125
|
+
# Pass a string or regexp <tt>:separator</tt> to specify a different separator of words:
|
126
|
+
#
|
127
|
+
# 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>')
|
128
|
+
# # => "Once<br>upon<br>a<br>time<br>in..."
|
129
|
+
#
|
130
|
+
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."):
|
131
|
+
#
|
132
|
+
# 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
|
133
|
+
# # => "And they found that many... (continued)"
|
134
|
+
def truncate_words(words_count, options = {})
|
135
|
+
sep = options[:separator] || /\s+/
|
136
|
+
sep = Regexp.escape(sep.to_s) unless Regexp === sep
|
137
|
+
if self =~ /\A((?>.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
|
138
|
+
$1 + (options[:omission] || "...")
|
139
|
+
else
|
140
|
+
dup
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|