## Copyright 2015 Bryan Colvin ... public domain so long as my name is present ##
#
# This gem collects various extensions from competing string gems, and my personal collection.
# Expect many future releases as new ideas/needs come to light
#
##
require "gstring/version"
require "setfu"
## ACTION ITEMS ##
# 1: replace options hash with array ... use *options so you can list them one-by-one
# 2: index, rindex ... add array of strings ... refactor parse to use new index
## FEATURES TODO NEXT REVISION ##
# 1: to_morse (maybe ... limited appeal my guess)
# 2: encryption? (maybe a separate gem, or include a wrapper from that gem)
# 3: format number to engineering notation ... 15530.to_eng => 1515.53e3
class String
@@gs_bracketing_pairs =
{
'[' => ']',
'(' => ')',
'{' => '}',
'<' => '>'
}
SI_UNIT_PREFIXES = {1=>'da', 2=>'h', 3=>'k', 6=>'M', 9=>'G', 12=>'T', 15=>'P', 18=>'E', 21=>'Z', 24=>'Y', 27=>'kY', 30=>'MY', 33=>'GY', 36=>'TY', 39=>'PY', 42=>'EY', 45=>'ZY', 48=>'YY',
-1=>'d',-2=>'c', -3=>'m', -6=>'µ',-9=>'n',-12=>'p',-15=>'f',-18=>'a',-21=>'z',-24=>'y',-27=>'my',-30=>'µy', -33=>'ny', -36=>'py', -39=>'fy', -42=>'ay', -45=>'zy', -48=>'yy'}
RGX_FLOAT = /\A[\+\-]?(0|[1-9]\d*)(([eE][\+\-]?\d+)|(\.\d+((e)?[\+\-]?\d+)?))/
STD_ESCAPE_SET_RUBY = [0..31,'"',"'","\\","\;","\#"].to_bset
STD_ESCAPE_HASH = {7=>"\\a", 8=>"\\b", 12=>"\\f", 10=>"\\n", 13=>"\\r", 9=>"\\t", 11=>"\\v"}
SET_PARSE_CHARS = BitSet.new.add_parse_chars!
SET_SPLIT_CHARS = SET_PARSE_CHARS | "_#`\""
SET_UPPERS = BitSet.uppercase_chars
SET_LOWERS = BitSet.lowercase_chars
SET_CHARS = BitSet.letter_chars
SET_INT_CHARS = BitSet.digit_chars
GS_SENTENCE_TERM = '?!.'.to_bset
GS_TITLE_EXCEPTIONS =
{
"a" =>true,
"an" =>true,
"and" =>true,
"as" =>true,
"at" =>true,
"but" =>true,
"by" =>true,
"for" =>true,
"from" =>true,
"in" =>true,
"nor" =>true,
"of" =>true,
"on" =>true,
"or" =>true,
"the" =>true,
"to" =>true,
"up" =>true
}
def self.reset_bracket_pairs
@@gs_bracketing_pairs =
{
'[' => ']',
'(' => ')',
'{' => '}',
'<' => '>'
}
end
def self.undefine_bracket_pair(str)
@@gs_bracketing_pairs.delete(str.first)
end
def self.define_bracket_pair(str)
@@gs_bracketing_pairs[str.first] = str.last
end
#ary.sort &String::inside_int_cmp
def self.inside_int_cmp(mode=true)
return lambda do |a,b|
a,b = (mode) ? [a.to_s, b.to_s] : [b.to_s, a.to_s]
if(a==b)
0
else
ta = a.dup
tb = b.dup
rgx = /\d+/
if (ta.find(rgx).nil? || tb.find(rgx).nil?)
a <=> b # standard compare
else # int inside one or both
rtn=0
loop do
if(ta==tb)
rtn=0
break
end
if(ta.empty? || tb.empty?)
rtn = ta <=> tb
break
end
la = ta.parse(rgx, :no_skip, :no_strip)
lb = tb.parse(rgx, :no_skip, :no_strip)
if(la != lb)
rtn = la <=> lb
break
end
if(ta.parsed != tb.parsed)
rtn = ta.parsed.to_i <=> tb.parsed.to_i
break
end
end #loop
rtn
end # if
end # if
end # lambda
end #self.inside_int_cmp
def self.chr_uni_esc(num)
num = num.to_i
return nil if num < 0
if num < 256
rtn = "\\x" + num.to_s(16).padto(2,'0',:left)
elsif num < 0x10000
rtn = "\\u" + num.to_s(16).padto(4,'0',:left)
elsif num < 0x1000000
rtn = "\\u" + num.to_s(16).padto(6,'0',:left)
else
rtn = "\\u" + num.to_s(16).padto(8,'0',:left)
end
end
SET_SUP_CHARS = (SET_CHARS | SET_INT_CHARS | "+-=()βγδεθινΦφχ") - "qCFQSXYZC"
SET_SUB_CHARS = "aeijoruvx" | SET_INT_CHARS | "βγρφχ()+-="
@@gs_sup_hash = {'β'=>'ᵝ', 'γ'=>'ᵞ', 'δ'=>'ᵟ', 'ε'=>'ᵋ', 'θ'=>'ᶿ', 'ι'=>'ᶥ', 'ν'=>'ᶹ', 'Φ'=>'ᶲ', 'φ'=>'ᵠ', 'χ'=>'ᵡ',
'1'=>'¹','2'=>'²','3'=>'³','0'=>'⁰','-'=>'⁻','+'=>'⁺', '='=>'⁼', '('=>'⁽', ')'=>'⁾',
'A'=>'ᴬ','B'=>'ᴮ','D'=>'ᴰ','E'=>'ᴱ','G'=>'ᴳ','H'=>'ᴴ','I'=>'ᴵ','J'=>'ᴶ','K'=>'ᴷ',
'L'=>'ᴸ','M'=>'ᴹ','N'=>'ᴺ','O'=>'ᴼ','P'=>'ᴾ','R'=>'ᴿ','T'=>'ᵀ','U'=>'ᵁ','V'=>'ⱽ','W'=>'ᵂ',
'a'=>'ᵃ','b'=>'ᵇ','c'=>'ᶜ','d'=>'ᵈ','e'=>'ᵉ','f'=>'ᶠ','g'=>'ᵍ','h'=>'ʰ',
'i'=>'ⁱ','j'=>'ʲ','k'=>'ᵏ','l'=>'ˡ','m'=>'ᵐ','n'=>'ⁿ','o'=>'ᵒ','p'=>'ᵖ','r'=>'ʳ',
's'=>'ˢ','t'=>'ᵗ','u'=>'ᵘ','v'=>'ᵛ','w'=>'ʷ','x'=>'ˣ','y'=>'ʸ','z'=>'ᶻ'}
@@gs_sub_hash = {'a'=>'ₐ', 'e'=>'ₑ','i'=>'ᵢ','j'=>'ⱼ','o'=>'ₒ','r'=>'ᵣ','u'=>'ᵤ','v'=>'ᵥ',
'x'=>'ₓ','β'=>'ᵦ','γ'=>'ᵧ','ρ'=>'ᵨ','φ'=>'ᵩ','χ'=>'ᵪ','='=>'₌','('=>'₍',')'=>'₎','+'=>'₊','-'=>'₋'}
(4..9).each do |i|
@@gs_sup_hash[(48+i).chr] = (0x2070+i).chr(Encoding::UTF_8)
end
(0..9).each do |i|
@@gs_sub_hash[(48+i).chr] = (0x2080+i).chr(Encoding::UTF_8)
end
def to_superscript(html=false)
return '' + self + '' if html
rtn = ""
each_char do |ch|
rtn += String::SET_SUP_CHARS.include?(ch) ? @@gs_sup_hash[ch] : ch
end
return rtn
end
def to_subscript(html=false)
return '' + self + '' if html
rtn = ""
each_char do |ch|
rtn += String::SET_SUB_CHARS.include?(ch) ? @@gs_sub_hash[ch] : ch
end
return rtn
end
def upcase?
set = self.to_bset
return false if String::SET_LOWERS ** set # may not have any lower
return nil unless String::SET_UPPERS ** set # must have at least one upper
return true
end
def downcase?
set = self.to_bset
return false if String::SET_UPPERS ** set # may not have any lower
return nil unless String::SET_LOWERS ** set # must have at least one lower
return true
end
def mixedcase?
set = self.to_bset
return nil unless set ** (String::SET_LOWERS | String::SET_UPPERS) # must have a letter
return (set ** String::SET_LOWERS) && (set ** String::SET_UPPERS)
end
# non-alpha chars must match too
def case_match(other) # true if string same size with same case at all positions
return false if (length != other.length)
length.times do |idx|
if (self[idx].upcase?)
return false unless other[idx].upcase?
elsif (self[idx].downcase?)
return false unless other[idx].downcase?
else # both must be letters at the position
return false if self[idx] != other[idx]
end
end
return true
end
def swapchar(pos, ch, *modes)
modes = modes.first if modes.first.class==Array
rtn = self[pos]
return nil if rtn.nil?
if ch.empty?
return nil unless modes.include? :empty_ok
end
unless ch.empty?
ch = ch.first unless modes.include? :substring
end
if modes.include? :casehold
if rtn.upcase?
self[pos]=ch.upcase
elsif rtn.downcase?
self[pos]=ch.downcase
else
self[pos]=ch.first
end
else
self[pos]=ch # standard mode
end
rtn
end
def find_all(obj, *modes) # returns an array of all positions of obj in self
pos = 0
ary = []
loop do
pos = self.index(obj, pos, *modes)
break if pos.nil?
ary.push pos
pos +=1
end
return ary
end
def find_near(pos, obj=String::SET_SPLIT_CHARS, *flags)
p_r = self.index(obj, pos, *flags)
p_l = self.rindex(obj, pos, *flags)
return [p_l,p_r]
end
def find_nth(obj, nth, *modes)
return nil if 0==nth
if nth > 0
pos = 0
loop do
nth -= 1
pos = self.index(obj, pos, *modes)
return nil if pos.nil?
return pos if nth.zero?
pos += 1
end
else
nth = -nth
pos = length
loop do
nth -= 1
pos = self.rindex(obj, pos, *modes)
return nil if pos.nil?
return pos if nth.zero?
pos -= 1
end
end
end
def cryptogram(dat=nil) # nil==> encode, string==>test for match
if (dat.nil?)
rtn = dup
set = BitSet.lowercase_chars
off_limits = []
skey = nil
ary = (self.downcase.to_bset & String::SET_LOWERS).to_a(false).shuffle
loop do
break if ary.empty?
skey = ary.pop
hits = find_all(skey,:ignore) - off_limits
off_limits = (off_limits | hits).sort
# edge case, set only has 1 element in it
rpw = (set.count==1) ? set.to_s : (set - skey).rand(1,:string)
set -= rpw #can only use an element once
hits.each do |pos|
rtn.swapchar(pos, rpw, :casehold)
end
end
return rtn
elsif (dat.class==String)
return false if self.length != dat.length
s1 = self.downcase.to_bset & BitSet.lowercase_chars
s2 = dat.downcase.to_bset & BitSet.lowercase_chars
return false if s1.count != s2.count
ary1 = self.downcase.find_all(s1)
ary2 = dat.downcase.find_all(s2)
return false if ary1 != ary2
return false unless case_match(dat)
length.times do |idx| # ::TODO:: find faster way ...
ary1 = self.find_all(self[idx],:ignore)
ary2 = dat.find_all(dat[idx],:ignore)
return false if ary1 != ary2
end
return true
else
return nil
end
nil
end
def cross_match(pat)
s_ptr=0
p_ptr=0
ch = ''
return nil unless pat.class==String
return false if empty?
return false if pat.empty?
loop do
return true if s_ptr >= self.length
return false if p_ptr >= pat.length
if ch != ''
if ch==self[s_ptr]
ch=""
s_ptr +=1
p_ptr +=1
if s_ptr >= self.length
return false if pat[p_ptr]=='+'
end
else
s_ptr +=1
return false if s_ptr >= self.length # ran out before match
end
elsif (pat[p_ptr]=='_')
p_ptr+=1
s_ptr+=1
elsif (pat[p_ptr]=='+')
s_ptr+=1 # skip next letter
p_ptr+=1
ch = pat[p_ptr]
return true if (ch.nil? || ch=='') #end of the line
elsif (pat[p_ptr]=='*')
p_ptr+=1
ch = pat[p_ptr]
return true if (ch.nil? || ch=='') #end of the line
elsif (pat[p_ptr]=='`')
p_ptr+=1
return false unless pat[p_ptr]==self[s_ptr]
p_ptr+=1
s_ptr+=1
else # compare chars
return false unless pat[p_ptr]==self[s_ptr]
p_ptr+=1
s_ptr+=1
end
end
return true
end
def condense!
strip!
str = ""
sp = false
each_char do |ch|
if (ch <= ' ')
sp = true
else
str += sp ? ' ' + ch : ch
sp = false
end
end
replace str
self
end
def condense
dup.condense!
end
def sort
self.split('').sort.join('')
end
def sort!
replace self.split('').sort.join('')
end
def histogram
hash = {}
self.split('').sort.each do |ch|
hash[ch] = hash[ch].nil? ? 1 : 1 + hash[ch]
end
return hash
end
def duplicates?
set = self.to_bset
return false if set.count == length
return true
end
def duplicates
str = self.sort
set = BitSet.new
rtn = ""
last_chr = ""
str.each_char do |ch|
if last_chr == ch
unless set.include? ch
rtn += ch
end
set.add! ch
end
last_chr = ch
end
return rtn
end
#ROT_STRING = ' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?/>.<,"\':;|\\}]{[+=_-)(*&^%$#@!`~Ω≈ç√∫˜µ≤≥÷åß∂ƒ©˙∆˚¬…æœ∑´®†¥¨ˆøπ“‘«`¡™£¢∞§¶•ªº–≠¸˛Ç◊ı˜Â¯˘¿ÅÍÎÏ˝ÓÔÒÚÆŒ„´‰ˇÁ¨ˆØ∏”’»`⁄€‹›fifl‡°·‚—±'
ROT_STRING = '{Copyright© 2016_BRYAN-cOLVIn=al|.®¡GHTS/4ME},+bd¯˘¿ÅÍe^%$f∛jΆ¥w¼∜x7Ï˝ÓÔk”’»⁄€‹×›fifl‡°·‚KPøπ“‘s½u!~Ω≈çzD∮5vF∂«`™£WJŒ„‰ˇÁ¨ˆØ∏3QℵU(*@√∫µ≤≥÷åßXZmqƒ˙∆˚¬…æœ∑´89?><"\':;\\][)¢∞§¶•ªº–≠¸˛Ç◊ı˜ÒÚÆ—±'
ROT_LEN = ROT_STRING.length
@@rot_hash = {}
ii = 0
ROT_STRING.each_char do |ch|
@@rot_hash[ch] = ii
ii += 1
end
def each_ord
unless block_given?
enu = Enumerator.new do |y|
self.each_char do |ch|
y << ch.ord
end
end
return enu
end
self.each_char do |ch|
yield ch.ord
end
end
def rehash # more consistent hash method
sum = 0
tr = 4294967296
rL = Random.new(self.length)
sum = rL.rand(tr)
self.each_char do |ch|
rt = Random.new(ch.ord)
sum += rt.rand(tr)
sum ^= rL.rand(sum)
end
return sum & (tr-1)
end
def rot_crypt(pswd="Standard Password should never be used!")
raise "illegal password" if pswd.empty?
rtn = ""
ptr = 0
rnd = Random.new(pswd.rehash)
len = pswd.length
self.each_char do |ch|
ss = @@rot_hash[ch]
if ss.nil? # make no subs
rtn += ch
else
# pos = (ch.ord + pswd[ptr].ord + rnd.rand(ROT_LEN)) % ROT_LEN
pos = (ss + pswd[ptr].ord + rnd.rand(ROT_LEN)) % ROT_LEN
rtn += ROT_STRING[pos]
end
ptr += 1
ptr = 0 if ptr >= len
end
return rtn
end
def rot_decrypt(pswd="Standard Password should never be used!")
raise "illegal password" if pswd.empty?
rtn = ""
ptr = 0
rnd = Random.new(pswd.rehash)
len = pswd.length
self.each_char do |ch|
ss = @@rot_hash[ch]
if ss.nil? # make no subs
rtn += ch
else
pos = @@rot_hash[ch] # got back the number
pos = pos - pswd[ptr].ord - rnd.rand(ROT_LEN)
pos = pos % ROT_LEN
# rtn += pos.chr(Encoding::UTF_8)
rtn += ROT_STRING[pos]
end
ptr += 1
ptr = 0 if ptr >= len
end
return rtn
end
###### DOES NOT WORK WITH RAILS ###################
# ... add new meaning to what would have raised an error ...
# now means case insensitive compare
# alias_method :old_string_rgx_cmp, :=~
# def =~(str)
# return old_string_rgx_cmp(str) unless str.class==String
# self.downcase == str.downcase
# end
####################################################
# replace with this ...
def cmp(other)
self <=> other
end
def cmpi(other)
self.upcase <=> other.upcase
end
def eqli?(other)
self.upcase == other.upcase
end
def equali?(other)
self.upcase == other.upcase
end
# generator does not need an instance
def self.random_password(chars=8, special="_-#!~@$%^*+=?:")
raise "password must be at least 8 characters" if chars < 8
low = BitSet.lowercase_chars
high = BitSet.uppercase_chars
digits = BitSet.digit_chars
special = special.to_bset rescue BitSet.new
all = low | high | digits | special
a,b = low.rand(2,:array_chars)
c,d = high.rand(2, :array_chars)
e = digits.rand(1, :array_chars)
f = special.rand(1, :array_chars)
pswd = [a,b,c,d,e]
pswd.push f unless (f.nil? || f.empty?)
filler = all.rand(chars - pswd.length, :array_chars)
filler.each do |ch|
pswd.push ch
end
pswd.shuffle!
return pswd.join ''
end
def remove!(obj, *prm) # remove all characters
flags = prm.dup
flags |= [:no_skip, :no_strip]
rtn = ""
str = ""
loop do
str += parse(obj,flags)
break if parsed.nil?
rtn += parsed
end
str += self
replace str
return rtn
end
def remove(obj, *prm) # remove all characters
str = dup
str.remove!(obj, *prm)
return str
end
def extract!(prm=1, fill_hole="")
return "" if empty?
if prm.kind_of? Integer # extract fron-end of string
return "" if prm < 1
prm = prm > length ? length : prm
rtn = self[0..(prm-1)]
# replace( fill_hole + self[prm..-1] ) # WRONG! ... we only want the same number of fill
replace( fill_hole[0..(prm-1)] + self[prm..-1] )
return rtn
elsif prm.class == Range
start = prm.first < 0 ? self.length + prm.first : prm.first
finish = prm.last < 0 ? self.length + prm.last : prm.last
if (start <= finish) # normal forward order
return "" if start >= length
finish = finish >= length ? length-1 : finish
return extract!(finish+1, fill_hole) if start==0 # was extract without the '!'
rtn = self[start..finish]
replace(self[0..(start-1)] + fill_hole + self[(finish+1)..-1])
return rtn
else # reverse order
return "" if finish >= length
start = start >= length ? length - 1 : start
rtn = self[finish..start].reverse
if(finish==0)
replace(fill_hole + self[(start+1)..-1])
else
replace(self[0..(finish-1)] + fill_hole + self[(start+1)..-1])
end
return rtn
end
elsif prm.class == Array
rtn = ""
# first count number of substitutions
cnt = 0
prm.each do |item|
if item.kind_of? Integer
cnt += 1
else
cnt += item.count
end
end
filler = fill_hole.padto(cnt, "\000", :right, :no_trunc) # use null as place holder
prm.each do |item|
if item.kind_of? Integer
pos = item < 0 ? 0 : item
if pos < length
rtn += self[pos]
self[pos]=filler.first!
end # ignore if out of range
else # use recursion
str = self.extract!(item, filler)
rtn += str
filler.extract! str.length # remove and discard
end
end
remove! "\000".to_bset
return rtn
else # convert to set
ary = (prm.to_bset & (0..(self.length-1))).to_a #ignore everything out of range
fill=fill_hole.dup
rtn = ""
oft = 0
ary.each do |idx|
ch = fill.extract!
rtn += self[idx-oft]
self[idx-oft]=ch
oft+=1 if ch.empty?
end
return rtn
end
return ""
end
def extract(prm)
return dup.extract!(prm, '')
end
def enclose(pairs, escape=nil, set=String::STD_ESCAPE_SET_RUBY, hash=String::STD_ESCAPE_HASH)
return self if pairs.empty?
if escape.nil?
return pairs.first + self + pairs.last
else # look for pairs.first , and replace with {escape}{pairs.first}
str = pairs.first
self.each_char do |ch|
if set.include? ch
idx = (set & ch).to_a.first
if (ch ** (set-[0..31]))
#byebug
str += "\\" + ch
elsif hash.include? idx
#byebug
str += hash[idx]
else # use hex format
str += String.chr_uni_esc(idx)
end
else
str += ch
end
end
return str + pairs.last
#str = self.gsub(pairs.first) { escape + pairs.first }
#return pairs.first + str + pairs.last
end
end
def enclose!(pairs, escape=nil)
replace enclose(pairs, escape)
end
def unenclose(escape=nil) # unescape
return nil if length < 2
unless first==last
return nil unless last == @@gs_bracketing_pairs[first]
end
if @@gs_bracketing_pairs.include? first
return nil if first==last
end
str = self[1..(length-2)]
unless escape.nil?
str = str.gsub(escape) { '' } # blind removal? ... may need to rethink this a tad
end
return str
end
def unenclose!(escape=nil)
str = unenclose(escape)
replace str unless str.nil?
str.nil? ? nil : self
end
def indent
cnt = 0
each_char do |ch|
break if ch > ' '
cnt += 1
end
cnt
end
def indent!
rtn = indent
lstrip!
return rtn
end
# ::todo:: Add flags :rotate, :stick
def padto(tar, *prms)
return self if length == tar
prms = prms.first if prms.first.kind_of? Array
with = ' '
flags = []
how = :left
prms.each do |prm|
if prm.kind_of? String
with=prm
next
end
case prm
when :no_trunc, :rotate, :stick, :space, :swing
flags.push prm
when :left, :right, :both
how = prm
else
raise "padto unknown parameter"
end
end
if length > tar
return self if flags.include? :no_trunc
return "" if tar<=0
return '…' if tar==1
return self[0..(tar-2)]+'…'
end
with = ' ' if with.empty?
rtn = ""
flags -= [:no_trunc]
flag = flags.empty? ? :stick : flags.first
with.access_mode(flag==:space ? :default : flag)
case how
when :right
rtn = self
(tar-length).times do |xx|
rtn += with.next
end
when :both
lp = (tar-length) >> 1
rp = (tar-length) - lp
str = ""
(rp).times do |xx|
str += with.next
end
rtn = str[0..(lp-1)].reverse + self + str
else # :left
str = ""
(tar-length).times do |xx|
str += with.next
end
rtn += str.reverse + self
end
rtn
end
def padto!(tar, *flags)
replace padto(tar, flags)
end
# negative len means position from right to left
# positive len means number of chars to capture
# exception if pos and len are both negative and pos > len
# ... then starting point is the |sum| from the right, and length
# ... and the ending point is also the |sum| ??? need more examples
def php_substr(pos=0,len=nil)
return "" if len==0
len = len.nil? ? length : len
r_s = (pos >= 0) ? pos : pos + length
r_e = (len >= 0) ? r_s + len - 1 : len + length - 1
return self[(r_s)..(r_e)]
end
def ignore_me
self
end
def parse(search_key = String::SET_PARSE_CHARS, *options)
options = options.first if options.first.class==Array
meth = options.include?(:no_strip) ? :ignore_me : :strip
rtn=""
@found = nil
if(search_key.class==BitSet)
#skip over first char
idx = options.include?(:no_skip) ? 0 : 1
sk = options.include?(:ignore) ? search_key.add_opposing_case : search_key
sf = idx
self[sf...length].each_char do |ch|
if(sk.include? ch)
@found=ch
break
end
idx += 1
end
unless @found.nil?
rtn = (idx==0) ? "" : self[0..(idx-1)].send(meth)
replace self[(idx+1)..-1].send(meth)
return rtn
end
rtn = clone
clear
return rtn
elsif (search_key.class == Regexp)
sf = options.include?(:no_skip) ? 0 : 1
pos = index(search_key,sf)
@found = pos.nil? ? nil : self.match(search_key)[0]
if pos.nil?
rtn = clone
clear
return rtn
else # found
rtn = pos.zero? ? "" : self[0..(pos-1)].send(meth)
replace self[(pos+@found.length)..length].send(meth)
return rtn
end
elsif (search_key.class == String)
#skip over first char unless option specifies :no_skip
sf = options.include?(:no_skip) ? 0 : 1
idx = index(search_key,sf,options)
if idx.nil?
rtn = clone
clear
return rtn
else
rtn = idx.zero? ? "" : self[0..(idx-1)].send(meth)
@found = self[idx..(idx+search_key.length-1)]
replace self[(idx+search_key.length)..length].send(meth)
return rtn
end
elsif (search_key.class == Array)
shortest = nil
best = nil
start = options.include?(:no_skip) ? 0 : 1
search_key.each_with_index do |val,idx|
pos = self.index(val,start,options)
unless pos.nil?
best = idx if best.nil?
shortest ||= pos
if (pos < shortest)
shortest = pos
best = idx
end
end
end
if shortest.nil?
rtn = clone
clear
return rtn
else
#opt = options.dup
#opt[:no_skip]=true
#return parse(search_key[best],opt)
return parse(search_key[best],options)
end
else # we are passed something that should have been converted to a set
return parse([search_key].to_bset,options)
end
end
## ##
# Updates to existing index, rindex methods #
# now accepts search types of: Set, Array #
## ##
alias_method :old_string_index_method_4gstring, :index
alias_method :old_string_rindex_method_4gstring, :rindex
def index(search,from=0,*options)
while options.first.class==Array
options = options.first
end
if (search.class==String)
return self.downcase.index(search.downcase,from) if (options.include? :ignore)
return old_string_index_method_4gstring(search,from)
end
if search.class==Array
best = nil
search.each_with_index do |item,idx|
pos = index(item,from,options)
unless pos.nil?
@best ||= idx
best ||= pos
if pos < best
best = pos
@best = idx
end
end
end
return best # parse uses this to know which item was found
end
# call this for unrecognized options
return old_string_index_method_4gstring(search,from) unless search.class == BitSet
if options.include? :ignore
return self.downcase.index(search.add_opposing_case,from)
end
if (from < 0)
from = length + from
end
from = 0 if from < 0
((from)...(length)).each do |ptr|
return ptr if search.include? self[ptr]
end
return nil
end
def rindex(search,from=-1,*options)
options = options.first if options.first.class==Array
if (search.class==String)
return self.downcase.rindex(search.downcase,from) if (options.include? :ignore)
return old_string_rindex_method_4gstring(search,from)
end
if search.class==Array
best = nil
search.each_with_index do |item,idx|
pos = rindex(item,from,options)
unless pos.nil?
@best ||= idx
best ||= pos
if pos > best
best = pos
@best = idx
end
end
end
return best # parse uses this to know which item was found
end
# call this for unrecognized options
return old_string_rindex_method_4gstring(search,from) unless search.class == BitSet
if options.include? :ignore
return self.downcase.rindex(search.add_opposing_case,from)
end
idx = (from < 0) ? length + from : from
return nil if idx < 0
until (search.include? (self[idx])) do
idx -= 1
return nil if idx < 0
end
return idx
end
alias_method :find, :index
alias_method :rfind, :rindex
def first
return "" if empty?
self[0]
end
def last
return "" if empty?
self[length-1]
end
# removes first char
def first!
return "" if empty?
rtn = self[0]
replace self[1..(length-1)]
return rtn
end
# removes last char
def last!
if length==1
rtn = dup
self.clear
return rtn
end
return "" if empty?
rtn = self[length-1]
replace self[0..(length-2)]
return rtn
end
def extract_trailing_int!
return nil if empty?
return nil unless String::SET_INT_CHARS.include? last
str = ""
while String::SET_INT_CHARS.include? last
str += last!
break if empty?
end
return str.reverse.to_i
end
def dec!(val=nil)
num = extract_trailing_int!
return self if num.nil?
return self if 0==num
val ||= 1
return self if (num-val) < 0
append!(num-val)
dup
end
def dec(val=nil)
return dup.dec!(val)
end
# increments integer at end of string
def inc!(val=nil)
num = extract_trailing_int!
if num.nil?
if val.nil?
append!(0)
else
append!(val)
end
else
append! (val.nil? ? (num+1) : (num+val))
end
dup
end
def inc(val=nil)
return dup.inc!(val)
end
def append(*prms)
rtn = ""
prms.each do |item|
rtn += item.to_s
end
self + rtn
end
def append!(*prms)
replace self.append(*prms)
end
def zap! # simple alias
clear
end
def parsed
@found ||= nil
end
def extract_leading_set!(set)
rtn = ""
set = set.to_bset
while set.include? first do
rtn += first!
end
return rtn
end
def extract_trailing_set!(set)
rtn = ""
set = set.to_bset
while set.include? last do
rtn += last!
end
return rtn.reverse
end
def split_on_set(set=' ')
str = dup
if (set.nil? || set.empty?)
set = ' '
end
set = set.to_bset
prefx = str.extract_leading_set!(set)
pstfx = str.extract_trailing_set!(set)
dat = []
tok = []
loop do
break if str.empty?
dat.push str.parse(set, :no_strip)
tmp = str.parsed
break if tmp.nil?
tmp += str.extract_leading_set!(set)
tok.push tmp
end
dat.push str unless str.empty?
return [dat,tok,prefx,pstfx]
end
def gs_titlecase
downcase!
if index(GS_SENTENCE_TERM)
ary = self.split_on_set(GS_SENTENCE_TERM)
pa = []
ary.first.each do |sentence|
pa.push(sentence.gs_titlecase.strip)
end
ary[0] = pa.reverse!
ary[1].reverse!
rtn = ary.first.pop
until ary.first.empty?
rtn += "#{ary[1].pop} #{ary.first.pop}"
end
rtn += ary.last || ""
return rtn
end
ary = self.split(' ')
ary.first.capitalize!
ary.last.capitalize!
(1..(ary.size-2)).each do |idx|
ary[idx].capitalize! unless GS_TITLE_EXCEPTIONS.include?(ary[idx])
end
return ary.join ' '
end
def gs_titlecase!
replace gs_titlecase
end
def shuffle
return self.split('').shuffle.join
end
def shuffle!
replace shuffle
end
def sublist(*prms)
if (prms.first.class==Symbol)
if prms.first == :bang
prms = prms[1]
else
raise "illegal sublist parameter"
end
end
prms.reverse!
cls = prms.pop
flags = {}
dflt = nil
subh = nil # substitution hash
suba = nil # substitution array
cnt = nil
until prms.empty?
tmp = prms.pop
if(tmp.class==Array)
raise "parameter error" unless suba.nil?
suba = tmp.reverse # make it easy to pop, and makes a new instance
elsif (tmp.class==Hash)
raise "parameter error" unless subh.nil?
subh = {}
tmp.each do |key,val|
subh[key.to_s] = val.to_s # convert keys and values to strings
end
elsif (tmp.class==Symbol)
flags[tmp] = true
elsif (tmp.class==Fixnum)
raise "count parameter repeated" unless cnt.nil?
cnt = tmp
elsif (tmp.class==String)
raise "parameter error" unless dflt.nil?
dflt = tmp
else
raise "unknown parameter type"
end
end
str = self.dup
ptr = 0
rtn = ""
cls = [cls] if (String==cls.class)
fa = []
# add more as needed later ...
fa.push :ignore if flags.include? :ignore
fass = fa + [:no_strip] + [:no_skip]
cls = [cls] if (BitSet==cls.class) # push everything into an array even regx
cls = [cls] if (Regexp==cls.class)
flst = (flags.include? :first) ? {} : nil
# remove nil values
subh ||= {}
suba ||= []
dflt ||= ""
if flags.include? :ignore # update substitution hash keys to lower case if :ignore
unless subh.empty?
tmp = {}
subh.each do |key,val|
tmp[key.downcase] = val.to_s
end
subh = tmp
end
end
unless (Hash==cls.class)
loop do
unless cnt.nil?
if (cnt <= 0)
rtn += str
return rtn
end
end
rtn += str.parse(cls, fass) # make sure we are working on arrays of a bunch of stuff including nested === add test case
break if str.parsed.nil?
unless flst.nil? # process first flag exceptions
key = (flags.include? :ignore) ? str.parsed.downcase : str.parsed
if flst.include? key
rtn += str.parsed # make no substitution
cnt -= 1 unless cnt.nil?
next
else
flst[key] = true # mark as taboo on future substitutions
end
end
# first check to see if we have a hash match
unless subh.empty?
key = (flags.include? :ignore) ? str.parsed.downcase : str.parsed
dat = subh[key]
unless dat.nil?
rtn += dat
return rtn if str.empty?
cnt -= 1 unless cnt.nil?
next
end
end
# next check to see if we have substitution data in the array
unless suba.empty?
rtn += suba.pop
return rtn if str.empty?
cnt -= 1 unless cnt.nil?
next
end
# at last use the default
if flags.include? :stop # do not make substitutions with default
rtn += str.parsed
rtn += str
return rtn
elsif flags.include? :skip # skip over, then keep going
rtn += str.parsed
else
rtn += dflt
end
return rtn if str.empty?
cnt -= 1 unless cnt.nil?
end # loop
return rtn
else # (Hash==cls.class)
cls = cls.dup
loop do
unless cnt.nil?
if (cnt <= 0)
rtn += str
return rtn
end
end
pos = nil
best = nil
bval = nil
cls.each do |key,val|
idx = str.index(key.to_s, 0 , fa)
unless idx.nil?
pos ||= idx
best ||= key.to_s
bval ||= val.to_s
if(idx < pos)
pos = idx
best = key.to_s
bval = val.to_s
end
break if idx.zero? # can't get better than zero, so stop looking
end
end
if best.nil?
rtn += str
return rtn
end
# byebug if flags.include? :ignore
rtn += str.parse(best, fass)
break if str.parsed.nil?
if flags.include? :first
cls.delete best
cls.delete best.to_sym # just in case
end
rtn += bval
cnt -= 1 unless cnt.nil?
end #loop
return rtn
end
end
def sublist!(*prms)
replace sublist(:bang, prms)
end
def to_num # convert to integer or float depending on format
self.dup.extract_num!
end
def extract_num!
dat = parse(String::RGX_FLOAT, :no_skip)
if parsed.nil? # no number found
num = dat.extract_leading_set!(BitSet.digit_chars)
replace dat
return 0 if num.empty?
return num.to_i
else
return parsed.to_f
end
end
def wrap_to(len, *flags)
if (self.length < len)
if flags.include? :array
return [self.dup]
else
return self.dup
end
end
ary = []
str = self.dup
if flags.include? :approximate
loop do
tar = str.find_near(len)
# now for the nasty end points and edge cases
if tar.first.nil?
if tar.last.nil?
ary.push str.extract! len
else
ary.push str.extract! tar.last + 1
end
else
if tar.last.nil?
ary.push str.extract! tar.first + 1
else # find closest fit
if (len - tar.first) <= (tar.last - len)
ary.push str.extract! tar.first + 1
else
ary.push str.extract! tar.last + 1
end
end
end
break if str.length <= len
end # loop
else
loop do
tar = str.rindex(String::SET_SPLIT_CHARS, len)
if (tar.nil?)
ary.push str.extract! len
else
ary.push str.extract! tar+1
end
break if str.length <= len
end #loop
end #if
ary.push str
ary.length.times { |ii| ary[ii].rstrip! }
return ary if flags.include? :array
if flags.include? :html
str = ary.join '
'
else
str = ary.join "\n"
end
return str
end
SET_VERTICLE = "\n\v".to_bset
def limit_to(size, *flags)
size = length + size + 1 if size < 0
if flags.first.kind_of? Integer
flags=flags.dup
cnt=flags.reverse!.pop
pos = find_nth(SET_VERTICLE,cnt,*flags)
if pos && (pos < size)
size = pos + 1
end
end
return self if length <= size
if flags.include? :no_break
pos = self.rfind(SET_SPLIT_CHARS,size-1)
return self[0..(size-2)] + '…' if pos.nil?
return self[0..(pos-1)] + '…'
else
return self[0..(size-2)] + '…'
end
end
def limit_to!(size, *flags)
replace limit_to(size, *flags)
end
def to_eng(pa=6, unit=nil)
return self.to_f.to_eng(pa, unit)
end
def to_scd(dp=nil, delim = ',.')
num = to_num
if(dp.nil?)
if num.class==Float
return num.to_scd(2,delim)
else
return num.to_scd(0,delim)
end
end
return num.to_scd(dp,delim)
end
def rand(cnt=1, *prms)
return "" if empty?
return "" if cnt < 1
if length==1
return self if prms.include? :set
return self * cnt
end
rnd = Random.new
rtn = ""
str = prms.include?(:set) ? self.to_bset.to_s : self.dup
cnt.times do
break if str.empty?
ch = str[rnd.rand(str.length)]
rtn += ch
str[str.find(ch)] = "" if prms.include? :once
end
return rtn
end
def rand!(cnt=1, *prms) # once is implied
return "" if empty?
return "" if cnt < 1
if length==1
return first!
end
rnd = Random.new
rtn = ""
cnt.times do
break if self.empty?
ch = self[rnd.rand(self.length)]
rtn += ch
if prms.include? :set
remove! ch.to_bset
else
self[self.find(ch)] = ""
end
end
return rtn
end
# default char for blank? or new mode
def access_mode(md=:stop, dflt=' ') # :swing :stick :default :rotate
@gs_access_mode = md
@gs_default_str = dflt
@gs_np_pos = nil
@gs_dir = :up
self
end
def next
@gs_np_pos ||= nil
@gs_access_mode ||= :stop
@gs_dir ||= :up
return "" if empty?
if @gs_np_pos.nil?
ch = first
@gs_np_pos = 0
return ch
else
case @gs_access_mode
when :stop
@gs_np_pos = @gs_np_pos >= length ? length : @gs_np_pos + 1
ch = self[@gs_np_pos]
return ch.nil? ? "" : ch
when :rotate
@gs_np_pos += 1
@gs_np_pos = @gs_np_pos >= length ? 0 : @gs_np_pos
return self[@gs_np_pos]
when :stick
@gs_np_pos = @gs_np_pos >= length ? length : @gs_np_pos + 1
ch = self[@gs_np_pos]
return ch.nil? ? last : ch
when :default
@gs_np_pos = @gs_np_pos >= length ? length : @gs_np_pos + 1
ch = self[@gs_np_pos]
return ch.nil? ? @gs_default_str : ch
when :swing
return first if length==1
if @gs_dir== :up
@gs_np_pos += 1
if @gs_np_pos >= length
@gs_dir= :down
@gs_np_pos = length - 2
end
return self[@gs_np_pos]
else # :down
@gs_np_pos -= 1
if @gs_np_pos < 0
@gs_dir= :up
@gs_np_pos = 1
end
return self[@gs_np_pos]
end
else
raise "unsupported access mode"
end
end
end
def prev
@gs_np_pos ||= nil
@gs_access_mode ||= :stop
@gs_dir ||= :up
return "" if empty?
if @gs_np_pos.nil?
ch = last
@gs_np_pos = length - 1
return ch
else
case @gs_access_mode
when :stop
@gs_np_pos = @gs_np_pos <= 0 ? -1 : @gs_np_pos - 1
return "" if @gs_np_pos < 0
return self[@gs_np_pos]
when :rotate
@gs_np_pos -= 1
@gs_np_pos = @gs_np_pos < 0 ? length-1 : @gs_np_pos
return self[@gs_np_pos]
when :stick
@gs_np_pos = @gs_np_pos <= 0 ? -1 : @gs_np_pos - 1
return first if @gs_np_pos < 0
return self[@gs_np_pos]
when :default
@gs_np_pos = @gs_np_pos <= 0 ? -1 : @gs_np_pos - 1
return @gs_default_str if @gs_np_pos < 0
return self[@gs_np_pos]
when :swing
return first if length==1
if @gs_dir== :down
@gs_np_pos += 1
if @gs_np_pos >= length
@gs_dir= :up
@gs_np_pos = length - 2
end
return self[@gs_np_pos]
else # :up
@gs_np_pos -= 1
if @gs_np_pos < 0
@gs_dir= :down
@gs_np_pos = 1
end
return self[@gs_np_pos]
end
else
raise "unsupported access mode"
end
end
end
def uchr
first
end
end # String
class Integer
def to_num
self
end
def to_scd(dp=0, delim = ',.')
return self.to_f.to_scd(dp, delim) unless dp.zero?
str = self.to_s.reverse.scan(/\d{1,3}/).join(delim.first).reverse
(self < 0) ? "-" + str : str
end
def to_eng(pa=6, unit=nil)
return self.to_f.to_eng(pa, unit)
end
def uchr
chr(Encoding::UTF_8)
end
def to_superscript(html=false)
to_s.to_superscript(html)
end
def to_subscript(html=false)
to_s.to_subscript(html)
end
end
class Float
def to_num
self
end
def to_scd(dp=2, delim = ',.')
num = ((10 ** dp) * self).round.to_s
man = num[0..(-dp-1)]
man = man.empty? ? '0' : man
frt = (num[(-dp)..(-1)] || "").padto(dp,'0',:right)
return man.reverse.scan(/\d{1,3}/).join(delim.first).reverse + delim.last + frt
end
def to_eng(pa=6, unit=nil)
pa = pa.to_i
pa = (pa<1) ? 1 : (pa>15) ? 15 : pa
if self < 0.0
num = -self
sgn = "-"
else
num = self
sgn = ""
end
str = "%.16e" % num
str.extract!(1..1) # remove decimal point
num = str.parse('e')
exp = str.to_i
pos = (exp%3)+1
if (exp < 0)
esgn = "-"
ee = -(exp/3)*3
else
esgn = ""
ee = (exp/3)*3
end
pd = pos > pa ? pos : pa
num = (num[0..pa].to_f/10).round.to_s[0..(pa-1)].padto(pd,'0',:right) # round to target size
num.insert(pos, '.') unless pos >= (num.length)
pfx = String::SI_UNIT_PREFIXES["#{esgn}#{ee}".to_i]
unless unit.nil?
unless pfx.nil?
num += pfx
ee=0 # disable 'e' thing
end
end
unit ||= ""
if (ee>0)
num += "e#{esgn}#{ee}"
end
return sgn+num+unit
end
end