#!/usr/local/bin/ruby -w
#
# == extensions/string.rb
#
# Adds methods to the builtin String class.
#
require "extensions/_base"
ExtensionsProject.implement(String, :leftmost_indent) do
class String
#
# Returns the size of the smallest indent of any line in the string.
# Emits a warning if tabs are found, and if $VERBOSE is on.
# You can use #expand_tabs to avoid this. This method is primarily intended
# for use by #tabto and is not likely to be all that useful in its own
# right.
#
def leftmost_indent
tabs_found = false
scan(/^([ \t]*)\S/).flatten.map { |ws|
tabs_found = true if ws =~ /\t/
ws.size
}.compact.min
ensure
if tabs_found and $VERBOSE
$stderr.puts %{
String#leftmost_indent: warning: tabs treated as spaces
(value: #{self.inspect[0..30]}...")
}.strip
end
end
protected :leftmost_indent
end
end
ExtensionsProject.implement(String, :expand_tabs) do
class String
#
# Expands tabs to +n+ spaces. Non-destructive. If +n+ is 0, then tabs are
# simply removed. Raises an exception if +n+ is negative.
#
#--
# Thanks to GGaramuno for a more efficient algorithm. Very nice.
def expand_tabs(n=8)
n = n.to_int
raise ArgumentError, "n must be >= 0" if n < 0
return gsub(/\t/, "") if n == 0
return gsub(/\t/, " ") if n == 1
str = self.dup
while
str.gsub!(/^([^\t\n]*)(\t+)/) { |f|
val = ( n * $2.size - ($1.size % n) )
$1 << (' ' * val)
}
end
str
end
end
end
ExtensionsProject.implement(String, :indent) do
class String
#
# Indents the string +n+ spaces.
#
def indent(n)
n = n.to_int
return outdent(-n) if n < 0
gsub(/^/, " "*n)
end
end
end
ExtensionsProject.implement(String, :outdent) do
class String
#
# Outdents the string +n+ spaces. Initial tabs will cause problems and
# cause a warning to be emitted (if warnings are on). Relative indendation
# is always preserved. Once the block hits the beginning of the line,
# that's it. In the following example, . represents space from the
# beginning of the line.
#
# str = %{
# ..One
# ....Two
# }.outdent(4)
#
# is
#
# One
# ..Two
#
def outdent(n)
n = n.to_int
return indent(-n) if n < 0
tabto(leftmost_indent - n)
end
end
end
ExtensionsProject.implement(String, :tabto) do
class String
#
# Move the string to the nth column. Relative indentation is preserved.
# Column indices begin at 0, so the result is that the leftmost character of
# the string has +n+ spaces before it.
#
# Examples:
# "xyz".tabto(0) # -> "xyz"
# "xyz".tabto(1) # -> " xyz"
# "xyz".tabto(2) # -> " xyz"
# " xyz".tabto(1) # -> " xyz"
#
# str = < true
# Hello, my name
# is Gerald.
# EOF
#
def tabto(n)
n = n.to_int
n = 0 if n < 0
find = " " * leftmost_indent()
replace = " " * (n)
gsub(/^#{find}/, replace)
end
end
end
ExtensionsProject.implement(String, :taballto) do
class String
#
# Tabs all lines in the string to column +n+. That is, relative indentation
# is _not_ preserved.
#
def taballto(n)
n = n.to_int
n = 0 if n < 0
gsub(/^[ \t]*/, " "*n)
end
end
end
ExtensionsProject.implement(String, :trim) do
class String
#
# Trims a string:
# - removes one initial blank line
# - removes trailing spaces on each line
# - if +margin+ is given, removes initial spaces up to and including
# the margin on each line, plus one space
#
# This is designed specifically for working with inline documents.
# Here-documents are great, except they tend to go against the indentation
# of your code. This method allows a convenient way of using %{}-style
# documents. For instance:
#
# USAGE = %{
# | usage: prog [-o dir] -h file...
# | where
# | -o dir outputs to DIR
# | -h prints this message
# }.trim("|")
#
# # USAGE == "usage: prog [-o dir] -h file...\n where"...
# # (note single space to right of margin is deleted)
#
# Note carefully that if no margin string is given, then there is no
# clipping at the beginning of each line and your string will remain
# indented. You can use tabto(0) to align it with the left of
# screen (while preserving relative indentation).
#
# USAGE = %{
# usage: prog [-o dir] -h file...
# where
# -o dir outputs to DIR
# -h prints this message
# }.trim.tabto(0)
#
# # USAGE == (same as last example)
#
def trim(margin=nil)
s = self.dup
# Remove initial blank line.
s.sub!(/\A[ \t]*\n/, "")
# Get rid of the margin, if it's specified.
unless margin.nil?
margin_re = Regexp.escape(margin || "")
margin_re = /^[ \t]*#{margin_re} ?/
s.gsub!(margin_re, "")
end
# Remove trailing whitespace on each line
s.gsub!(/[ \t]+$/, "")
s
end
end
end
ExtensionsProject.implement(String, :starts_with?) do
class String
#
# Returns true iff this string starts with +str+.
# "Hello, world".starts_with?("He") # -> true
# "Hello, world".starts_with?("Green") # -> false
#
def starts_with?(str)
str = str.to_str
head = self[0, str.length]
head == str
end
end
end
ExtensionsProject.implement(String, :ends_with?) do
class String
#
# Returns true iff this string ends with +str+.
# "Hello, world".ends_with?(", world") # -> true
# "Hello, world".ends_with?("Green") # -> false
#
def ends_with?(str)
str = str.to_str
tail = self[-str.length, str.length]
tail == str
end
end
end
ExtensionsProject.implement(String, :line) do
class String
#
# Returns a line or lines from the string. +args+ can be a single integer,
# two integers or a range, as per Array#slice. The return value is
# a single String (a single line), an array of Strings (multiple lines) or
# +nil+ (out of bounds). Note that lines themselves do not contain a
# trailing newline character; that is metadata. Indexes out of bounds are
# ignored.
#
# data = " one \n two \n three \n four \n five \n"
# data.line(1) # -> " two "
# data.line(0,1) # -> [" one "]
# data.line(3..9) # -> [" four ", " five "]
# data.line(9) # -> nil
#
def line(*args)
self.split(/\n/).slice(*args)
rescue TypeError
raise TypeError,
"String#line(*args): args must be one Integer, two Integers or a Range"
rescue ArgumentError
raise ArgumentError,
"String#line(*args): args must be one Integer, two Integers or a Range"
end
end
end
ExtensionsProject.implement(String, :cmp) do
class String
#
# Compare this string to +other+, returning the first index at which they
# differ, or +nil+ if they are equal.
#
# "practise".cmp("practice") # -> 6
# "noun".cmp("nouns") # -> 5 (and vice versa)
# "fly".cmp("fly") # -> nil
#
def cmp(other)
other = other.to_str
if self == other
return nil
else
n = [self.size, other.size].min
(0..n).each do |i|
return i unless self[i] == other[i]
end
end
end
end
end
ExtensionsProject.implement(String, :join) do
class String
#
# Join all the lines of the string together, and compress spaces. The resulting string
# will have no surrounding whitespace.
#
# text = %{
# Once upon a time,
# Little Red Riding Hood ...
#
# }
#
# text.join # -> "Once upon a time, Little Red Riding Hood ..."
#
def join
gsub(/([ \t]*\n[ \t]*)+/, ' ').strip
end
end
end