webget-string_extensions 1.0.2 → 1.0.4
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/lib/string_extensions.rb +73 -143
- data/test/unit/string_extensions_test.rb +37 -0
- metadata +2 -2
data/lib/string_extensions.rb
CHANGED
@@ -7,7 +7,6 @@
|
|
7
7
|
#
|
8
8
|
# Ruby String base class extensions
|
9
9
|
#
|
10
|
-
# For #prev and #pred see http://snippets.dzone.com/posts/show/2474
|
11
10
|
##
|
12
11
|
|
13
12
|
class String
|
@@ -24,156 +23,87 @@ class String
|
|
24
23
|
split(/\b/).map{|x| x.capitalize }.join
|
25
24
|
end
|
26
25
|
|
26
|
+
# Increment the rightmost integer in a string, if any
|
27
|
+
#
|
28
|
+
# cf. String#succ
|
29
|
+
# cf. String#prev
|
30
|
+
def increment(step=1)
|
31
|
+
sub(/(.*)(\d+)/){|s| $1+((($2.to_i)+step).to_s) }
|
32
|
+
end
|
27
33
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# Items in the middle of a string don't do this - e.g. "12.10" goes to
|
36
|
-
# "12.09", to match how "next" would work as best as possible.
|
37
|
-
#
|
38
|
-
# The standard "next" function works on strings that contain *no*
|
39
|
-
# alphanumeric characters, using character codes. This implementation
|
40
|
-
# of "prev" does *not* work on such strings - while strings may contain
|
41
|
-
# any characters you like, only the alphanumeric components are operated
|
42
|
-
# upon.
|
43
|
-
#
|
44
|
-
# Should total underflow result, "nil" will be returned - e.g. "00".prev
|
45
|
-
# returns 'nil', as does "a".prev. This is done even if there are other
|
46
|
-
# characters in the string that were not touched - e.g. "+0.0".prev
|
47
|
-
# also returns "nil". Broadly speaking, a "nil" return value is used for
|
48
|
-
# any attempt to find the previous value of a string that could not have
|
49
|
-
# been generated using "next" in the first place.
|
50
|
-
#
|
51
|
-
# As with "next" sometimes the result of "prev" can be a little obscure
|
52
|
-
# so it is often best to try out things using "irb" if unsure. Note in
|
53
|
-
# particular that software revision numbers do not necessarily behave
|
54
|
-
# predictably, because they don't with "next"! E.g. "12.4.9" might go to
|
55
|
-
# "12.4.10" for a revision number, but "12.4.9".next = "12.5.0". Thus
|
56
|
-
# "12.5.0".prev = "12.4.9" and "12.4.10".prev = "12.4.09" (because the
|
57
|
-
# only way to make "12.4.10" using "next" is to start at "12.4.09").
|
58
|
-
#
|
59
|
-
# Since 'succ' (successor) is an alias for 'next', so 'pred'
|
60
|
-
# (predecessor) is an alias for 'prev'.
|
61
|
-
#
|
62
|
-
def prev(collapse = false)
|
63
|
-
str = self.dup
|
64
|
-
early_exit = false
|
65
|
-
any_done = false
|
66
|
-
ranges = [
|
67
|
-
('0'[0]..'9'[0]),
|
68
|
-
('a'[0]..'z'[0]),
|
69
|
-
('A'[0]..'Z'[0]),
|
70
|
-
nil
|
71
|
-
]
|
72
|
-
|
73
|
-
# Search forward for the first in-range character. If found check
|
74
|
-
# to see if that character is "1", "a" or "A". If it is, record
|
75
|
-
# its index (from 0 to string length - 1). We'll need this if
|
76
|
-
# underflows wrap as far as the found byte because in that case
|
77
|
-
# this first found byte should be deleted ("aa..." -> "z...",
|
78
|
-
# "10..." -> "9...").
|
79
|
-
|
80
|
-
first_ranged = nil
|
81
|
-
|
82
|
-
for index in (1..str.length)
|
83
|
-
byte = str[index - 1]
|
84
|
-
|
85
|
-
# Determine whether or not the current byte is a number, lower case
|
86
|
-
# or upper case letter. We expect 'select' to only find one matching
|
87
|
-
# array entry in 'ranges', thus we dereference index 0 after the
|
88
|
-
# 'end' to put a matching range from within 'ranges' into 'within',
|
89
|
-
# or 'nil' for any unmatched byte.
|
90
|
-
|
91
|
-
within = ranges.select do |range|
|
92
|
-
range.nil? or range.include?(byte)
|
93
|
-
end [0]
|
94
|
-
|
95
|
-
unless within.nil?
|
96
|
-
case within.first
|
97
|
-
when '0'[0]
|
98
|
-
match_byte = '1'[0]
|
99
|
-
else
|
100
|
-
match_byte = within.first
|
101
|
-
end
|
102
|
-
|
103
|
-
first_ranged = index - 1 if (byte == match_byte)
|
104
|
-
first_within = within
|
105
|
-
break
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
for index in (1..str.length)
|
110
|
-
|
111
|
-
# Process the input string in reverse character order - fetch the
|
112
|
-
# bytes via negative index.
|
113
|
-
|
114
|
-
byte = str[-index]
|
115
|
-
|
116
|
-
within = ranges.select do |range|
|
117
|
-
range.nil? or range.include?(byte)
|
118
|
-
end [0]
|
119
|
-
|
120
|
-
# Skip this letter unless within a known range. Otherwise note that
|
121
|
-
# at least one byte was able to be processed.
|
122
|
-
|
123
|
-
next if within.nil?
|
124
|
-
any_done = true
|
125
|
-
|
126
|
-
# Decrement the current byte. If it is still within its range, set
|
127
|
-
# the byte and bail out - we're finished. Flag the early exit. If
|
128
|
-
# the byte is no longer within range, wrap ththe character around
|
129
|
-
# and continue the loop to carry the decrement to an earlier byte.
|
130
|
-
|
131
|
-
byte = byte - 1
|
132
|
-
|
133
|
-
if (within.include? byte)
|
134
|
-
str[-index] = byte
|
135
|
-
early_exit = true
|
136
|
-
break
|
137
|
-
else
|
138
|
-
str[-index] = within.last
|
139
|
-
|
140
|
-
# If we've just wrapped around a character immediately after the
|
141
|
-
# one found right at the start ('0', 'a' or 'A') then this first
|
142
|
-
# ranged character should be deleted (so "10" -> "09"
|
143
|
-
|
144
|
-
if (first_ranged != nil and first_within.include?(byte + 1) and (first_ranged - str.length) == -(index + 1))
|
145
|
-
str.slice!(-(index + 1))
|
146
|
-
early_exit = true
|
147
|
-
break
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
end # From outer 'for' loop
|
34
|
+
# Decrement the rightmost integer in a string, if any
|
35
|
+
#
|
36
|
+
# cf. String#succ
|
37
|
+
# cf. String#prev
|
38
|
+
def decrement(step=1)
|
39
|
+
sub(/(.*)(\d+)/){|s| $1+((($2.to_i)-step).to_s) }
|
40
|
+
end
|
152
41
|
|
153
|
-
# If we did process at least one byte but we did not exit early, then
|
154
|
-
# the loop completed due to carrying a decrement to other bytes. This
|
155
|
-
# means an underflow condition - return 'nil'.
|
156
42
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
43
|
+
# Return the previous character, with a changed flag and carry flag
|
44
|
+
#
|
45
|
+
# ==Examples
|
46
|
+
# String.prev_char('n') => 'm', true, false # change
|
47
|
+
# String.prev_char('a') => 'z', true, true # change & carry
|
48
|
+
# String.prev_char('6') => '5', true, false # change
|
49
|
+
# String.prev_char('0') => '9', true, true # change & carry
|
50
|
+
# String.prev_char('-') => '-', false, false # unchanged
|
51
|
+
|
52
|
+
def self.prev_char(c) #=> prev_char, changed_flag, carry_flag
|
53
|
+
case c
|
54
|
+
when '1'..'9', 'B'..'Z', 'b'..'z'
|
55
|
+
return (c[0]-1).chr, true, false
|
56
|
+
when '0'
|
57
|
+
return '9', true, true
|
58
|
+
when 'A'
|
59
|
+
return 'Z', true, true
|
60
|
+
when 'a'
|
61
|
+
return 'z', true, true
|
62
|
+
else
|
63
|
+
return c, false, false
|
162
64
|
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return the previous string
|
68
|
+
#
|
69
|
+
# c.f. String#next
|
70
|
+
#
|
71
|
+
# ==Examples
|
72
|
+
# '888'.prev => '887'
|
73
|
+
# 'n'.prev => 'm'
|
74
|
+
# 'N'.prev => 'M'
|
75
|
+
#
|
76
|
+
# ==Examples with carry
|
77
|
+
# '880'.prev => '879'
|
78
|
+
# 'nna'.prev => 'nmz'
|
79
|
+
# 'NNA'.prev => 'NMZ'
|
80
|
+
# 'nn0aA'.prev => 'nm9zZ'
|
81
|
+
|
82
|
+
def prev
|
83
|
+
self.clone.prev!
|
84
|
+
end
|
163
85
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
#
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
return self
|
86
|
+
# Do String#prev in place
|
87
|
+
def prev!
|
88
|
+
return self if length==0
|
89
|
+
i=length-1 # rightmost
|
90
|
+
while true do
|
91
|
+
c=self[i].chr
|
92
|
+
prev_c,changed_flag,carry_flag=String.prev_char(c)
|
93
|
+
return self if !changed_flag
|
94
|
+
self[i]=prev_c
|
95
|
+
return self if !carry_flag
|
96
|
+
i-=1
|
97
|
+
return nil if i<0
|
172
98
|
end
|
99
|
+
end
|
173
100
|
|
174
|
-
|
175
|
-
|
101
|
+
alias pred prev # String#pred : predecessor :: String#succ : successor
|
102
|
+
alias pred! prev!
|
176
103
|
|
104
|
+
class << self
|
105
|
+
alias_method :pred_char, :prev_char
|
106
|
+
end
|
177
107
|
|
178
108
|
end
|
179
109
|
|
@@ -15,7 +15,44 @@ class StringExtensionsTest < Test::Unit::TestCase
|
|
15
15
|
assert_equal("Foo Goo Hoo","foo goo hoo".capitalize_words)
|
16
16
|
end
|
17
17
|
|
18
|
+
def test_increment
|
19
|
+
assert_equal('5','4'.increment, 'number is entire string')
|
20
|
+
assert_equal('foo5','foo4'.increment, 'number is rightmost')
|
21
|
+
assert_equal('5foo','4foo'.increment, 'number is leftmost')
|
22
|
+
assert_equal('foo5bar','foo4bar'.increment, 'number is in then middle')
|
23
|
+
assert_equal('foobar','foobar'.increment, 'no number, so should be unchanged')
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_decrement
|
27
|
+
assert_equal('3','4'.decrement, 'number is entire string')
|
28
|
+
assert_equal('foo3','foo4'.decrement, 'number is rightmost')
|
29
|
+
assert_equal('3foo','4foo'.decrement, 'number is leftmost')
|
30
|
+
assert_equal('foo3bar','foo4bar'.decrement, 'number is in then middle')
|
31
|
+
assert_equal('foobar','foobar'.decrement, 'no number, so should be unchanged')
|
32
|
+
end
|
18
33
|
|
34
|
+
def test_prev_char
|
35
|
+
assert_equal(["-",false,false] ,String.prev_char("-")) #unchanged
|
36
|
+
assert_equal(["5",true,false] ,String.prev_char("6")) #numeric typical
|
37
|
+
assert_equal(["9",true,true] ,String.prev_char("0")) #numeric carry
|
38
|
+
assert_equal(["m",true,false] ,String.prev_char("n")) #lowercase typical
|
39
|
+
assert_equal(["z",true,true] ,String.prev_char("a")) #lowercase carry
|
40
|
+
assert_equal(["M",true,false] ,String.prev_char("N")) #uppercase typical
|
41
|
+
assert_equal(["Z",true,true] ,String.prev_char("A")) #uppercase carry
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_prev
|
45
|
+
assert_equal('n-','n-'.prev) # unchanged
|
46
|
+
assert_equal('n5','n6'.prev) # numeric typical
|
47
|
+
assert_equal('m9','n0'.prev) # numeric carry
|
48
|
+
assert_equal('m999','n000'.prev) # numeric carries
|
49
|
+
assert_equal('ne','nf'.prev) # lowercase typical
|
50
|
+
assert_equal('mz','na'.prev) # lowercase carry
|
51
|
+
assert_equal('mzzz','naaa'.prev) # lowercase carries
|
52
|
+
assert_equal('NE','NF'.prev) # uppercase typical
|
53
|
+
assert_equal('MZ','NA'.prev) # uppercase carry
|
54
|
+
assert_equal('MZZZ','NAAA'.prev) # uppercase carries
|
55
|
+
end
|
19
56
|
|
20
57
|
end
|
21
58
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: webget-string_extensions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- WebGet
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-02-
|
12
|
+
date: 2009-02-13 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|