## 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 SET_LIKE_KEEP = SET_CHARS | SET_INT_CHARS | "*-'" #------------------------------------------------- def like(table_class_name, table_column_name, mode_and=true, kp = SET_LIKE_KEEP) ary = [] self.split.each do |substr| str = substr.keep kp if (str.first=='*') str.first! if (str.last=='*') str.last! ary.push "%#{str}%".gsub('*', '%') else ary.push "%#{str}".gsub('*', '%') end elsif (str.last=='*') str.last! ary.push "#{str}%".gsub('*', '%') else ary.push "%#{str}%".gsub('*', '%') end end #each md = mode_and ? ' and ' : ' or ' qry = (["`#{table_column_name}` like ?"] * ary.count).join md return table_class_name.where(qry, *ary) end R_RATED = /(cum[\-_s5]|pi[s5][s5]|f.ck|4k.r|whore|slut|sh[iy]t|n[i1l][g9][g9]er|qqqq|my.*a[sz]|ph.ck|dick|vagina|a[sz5][sz5].*wipe|a[sz5][sz5].*hole|penis|b4i4q)/i def clean?(rgx = R_RATED) return self.scan(rgx).empty? 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 push(str) replace(self+str).dup end def pop(nchars=1) if (nchars < 1) # avoid silly requests return "" elsif (nchars >= length) rtn = dup replace "" return rtn end rtn = self[-nchars..-1] replace self[0..(length-nchars-1)] return rtn 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 keep(ks) # only keep chars belonging to set rtn = "" self.each_char do |ch| rtn += ch if ks.include? ch end return rtn end def keep!(ks) replace keep(ks) 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 to_crlf rtn = "" state = :normal each_char do |ch| if :normal == state if "\r" == ch state = :r_pending elsif "\n" == ch rtn += "\r\n" else rtn += ch end else if "\n" == ch rtn += "\r\n" else rtn += "\r" rtn += ch end state = :normal end end return rtn end def to_lf # convert \r\n to \n rtn = "" state = :normal each_char do |ch| if :normal == state if "\r" == ch state = :r_pending elsif "\n" == ch rtn += "\012" else rtn += ch end else if "\n" == ch rtn += "\n" else rtn += "\r" rtn += ch end state = :normal end end return rtn end def to_crlf! self.replace to_crlf self end def to_lf! # removes \r self.replace to_lf self end SET_COMPACT_RIGHT = "<[{(".to_bset SET_COMPACT_LEFT = ".,!?:;>]})'\"\\/|-_+".to_bset def compact(sl=SET_COMPACT_LEFT, sr=SET_COMPACT_RIGHT) #sl = sl.to_bset #sr = sr.to_bset rtn = "" state = :start each_char do |ch| if :start == state next if ch <= ' ' state = state = sr.include?(ch) ? :right : :add rtn += ch elsif :add == state if ch <= ' ' state = :zap rtn += ' ' elsif sr.include? ch state = :right rtn += ch else rtn += ch end elsif :right == state unless ch <= ' ' rtn += ch state = sr.include?(ch) ? :right : :add end else #elsif :zap == state next if ch <= ' ' rtn.pop if sl.include? ch # state = :add state = sr.include?(ch) ? :right : :add rtn += ch end end rtn.pop if rtn.last==' ' return rtn end def compact!(sl=SET_COMPACT_LEFT, sr=SET_COMPACT_RIGHT) replace compact(sl, sr) end def wrap_to(len, *flags) # remove this as cr will mess things up # the logic below will need to be inferred as needed #if (self.length < len) # if flags.include? :array # return [self.dup] # else # return self.dup # end #end ary = [] str = self.to_lf # remove messy \r if present if flags.include? :approximate loop do tar = str.find_near(len) tmp = str.find("\n") unless tmp.nil? tar = [tmp,tmp] if tmp <= len end # 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) tmp = str.find("\n") unless tmp.nil? tar = tmp if tmp <= len end 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].strip! } unless flags.include? :no_strip 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