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.
@@ -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
- # The opposite of String::next / String::succ. It is impossible to be a
29
- # *complete* opposite because both "9".next = "10" and "09".next = "10";
30
- # if going backwards from "10" there's no way to know whether the result
31
- # should be "09" or "9". Where the first ranged character is about to
32
- # underflow and the next character is within the same range the result
33
- # is shrunk down - that is, "10" goes to "9", "aa" goes to "z"; any non-
34
- # range prefix or suffix is OK, e.g. "+!$%10-=+" goes to "+!$%9-=+".
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
- if (any_done == true and early_exit == false)
158
- return nil
159
- else
160
- return str
161
- end
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
- # As (extended) String::pred / String::prev, but modifies the string in
165
- # place rather than returning a copy. If underflow occurs, the string
166
- # will be unchanged. Returns 'self'.
167
- #
168
- def prev!
169
- new_str = prev
170
- self.replace(new_str) unless new_str.nil?
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
- alias pred prev
175
- alias pred! prev!
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.2
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-05 00:00:00 -08:00
12
+ date: 2009-02-13 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies: []
15
15