Ternary operator - ruby

I have an array d = ['foo', 'bar', 'baz'], and want to put its elements together into a string delimited by , and and at the last element so that it will become foo, bar and baz.
Here is what I'm trying to do:
s = ''
d.each_with_index { |x,i|
s << x
s << i < d.length - 1? i == d.length - 2 ? ' and ' : ', ' : ''
}
but the interpreter gives an error:
`<': comparison of String with 2 failed (ArgumentError)
However, it works with += instead of <<, but the Ruby Cookbook says that:
If efficiency is important to you, don't build a new string when you can append items onto an existing string. [And so on]... Use str << var1 << ' ' << var2 instead.
Is it possible without += in this case?
Also, there has to be a more elegant way of doing this than the code above.

You're just missing some parenthesis:
d = ['foo', 'bar', 'baz']
s = ''
d.each_with_index { |x,i|
s << x
s << (i < d.length - 1? (i == d.length - 2 ? ' and ' : ', ') : '')
}

I'd find
s << i < d.length - 1? i == d.length - 2 ? ' and ' : ', ' : ''
hard to read or maintain.
I'd probably change it to
join = case
when i < d.length - 2 then ", "
when i == d.length - 2 then " and "
when i == d.length then ""
end
s << join
Or possibly do
earlier_elements = d[0..-2].join(", ")
s = [earlier_elements, d[-1..-1]].join(" and ")
Or
joins = [", "] * (d.length - 2) + [" and "]
s = d.zip(joins).map(&:join).join

So much simpler like this:
"#{d[0...-1].join(", ")} and #{d.last}"

Related

Globbing using braces on Ruby 1.9.3

Recent versions of Ruby support the use of braces in globbing, if you use the File::FNM_EXTGLOB option
From the 2.2.0 documentation
File.fnmatch('c{at,ub}s', 'cats', File::FNM_EXTGLOB) #=> true # { } is supported on FNM_EXTGLOB
However, the 1.9.3 documentation says it isn't supported in 1.9.3:
File.fnmatch('c{at,ub}s', 'cats') #=> false # { } isn't supported
(also, trying to use File::FNM_EXTGLOB gave a name error)
Is there any way to glob using braces in Ruby 1.9.3, such as a third-party gem?
The strings I want to match against are from S3, not a local file system, so I can't just ask the operating system to do the globbing as far as I know.
I'm in the process of packaging up a Ruby Backport for braces globbing support. Here are the essential parts of that solution:
module File::Constants
FNM_EXTGLOB = 0x10
end
class << File
def fnmatch_with_braces_glob(pattern, path, flags =0)
regex = glob_convert(pattern, flags)
return regex && path.match(regex).to_s == path
end
def fnmatch_with_braces_glob?(pattern, path, flags =0)
return fnmatch_with_braces_glob(pattern, path, flags)
end
private
def glob_convert(pattern, flags)
brace_exp = (flags & File::FNM_EXTGLOB) != 0
pathnames = (flags & File::FNM_PATHNAME) != 0
dot_match = (flags & File::FNM_DOTMATCH) != 0
no_escape = (flags & File::FNM_NOESCAPE) != 0
casefold = (flags & File::FNM_CASEFOLD) != 0
syscase = (flags & File::FNM_SYSCASE) != 0
special_chars = ".*?\\[\\]{},.+()|$^\\\\" + (pathnames ? "/" : "")
special_chars_regex = Regexp.new("[#{special_chars}]")
if pattern.length == 0 || !pattern.index(special_chars_regex)
return Regexp.new(pattern, casefold || syscase ? Regexp::IGNORECASE : 0)
end
# Convert glob to regexp and escape regexp characters
length = pattern.length
start = 0
brace_depth = 0
new_pattern = ""
char = "/"
loop do
path_start = !dot_match && char[-1] == "/"
index = pattern.index(special_chars_regex, start)
if index
new_pattern += pattern[start...index] if index > start
char = pattern[index]
snippet = case char
when "?" then path_start ? (pathnames ? "[^./]" : "[^.]") : ( pathnames ? "[^/]" : ".")
when "." then "\\."
when "{" then (brace_exp && (brace_depth += 1) >= 1) ? "(?:" : "{"
when "}" then (brace_exp && (brace_depth -= 1) >= 0) ? ")" : "}"
when "," then (brace_exp && brace_depth >= 0) ? "|" : ","
when "/" then "/"
when "\\"
if !no_escape && index < length
next_char = pattern[index += 1]
special_chars.include?(next_char) ? "\\#{next_char}" : next_char
else
"\\\\"
end
when "*"
if index+1 < length && pattern[index+1] == "*"
char += "*"
if pathnames && index+2 < length && pattern[index+2] == "/"
char += "/"
index += 2
"(?:(?:#{path_start ? '[^.]' : ''}[^\/]*?\\#{File::SEPARATOR})(?:#{!dot_match ? '[^.]' : ''}[^\/]*?\\#{File::SEPARATOR})*?)?"
else
index += 1
"(?:#{path_start ? '[^.]' : ''}(?:[^\\#{File::SEPARATOR}]*?\\#{File::SEPARATOR}?)*?)?"
end
else
path_start ? (pathnames ? "(?:[^./][^/]*?)?" : "(?:[^.].*?)?") : (pathnames ? "[^/]*?" : ".*?")
end
when "["
# Handle character set inclusion / exclusion
start_index = index
end_index = pattern.index(']', start_index+1)
while end_index && pattern[end_index-1] == "\\"
end_index = pattern.index(']', end_index+1)
end
if end_index
index = end_index
char_set = pattern[start_index..end_index]
char_set.delete!('/') if pathnames
char_set[1] = '^' if char_set[1] == '!'
(char_set == "[]" || char_set == "[^]") ? "" : char_set
else
"\\["
end
else
"\\#{char}"
end
new_pattern += snippet
else
if start < length
snippet = pattern[start..-1]
new_pattern += snippet
end
end
break if !index
start = index + 1
end
begin
return Regexp.new("\\A#{new_pattern}\\z", casefold || syscase ? Regexp::IGNORECASE : 0)
rescue
return nil
end
end
end
This solution takes into account the various flags available for the File::fnmatch function, and uses the glob pattern to build a suitable Regexp to match the features. With this solution, these tests can be run successfully:
File.fnmatch('c{at,ub}s', 'cats', File::FNM_EXTGLOB)
#=> true
File.fnmatch('file{*.doc,*.pdf}', 'filename.doc')
#=> false
File.fnmatch('file{*.doc,*.pdf}', 'filename.doc', File::FNM_EXTGLOB)
#=> true
File.fnmatch('f*l?{[a-z].doc,[0-9].pdf}', 'filex.doc', File::FNM_EXTGLOB)
#=> true
File.fnmatch('**/.{pro,}f?l*', 'home/.profile', File::FNM_EXTGLOB | File::FNM_DOTMATCH)
#=> true
The fnmatch_with_braces_glob (and ? variant) will be patched in place of fnmatch, so that Ruby 2.0.0-compliant code will work with earlier Ruby versions, as well. For clarity reasons, the code shown above does not include some performance improvements, argument checking, or the Backports feature detection and patch-in code; these will obviously be included in the actual submission to the project.
I'm still testing some edge cases and heavily optimizing performance; it should be ready to submit very soon. Once it's available in an official Backports release, I'll update the status here.
Note that Dir::glob support will be coming at the same time, as well.
That was a fun Ruby exercise!
No idea if this solution is robust enough for you, but here goes :
class File
class << self
def fnmatch_extglob(pattern, path, flags=0)
explode_extglob(pattern).any?{|exploded_pattern|
fnmatch(exploded_pattern,path,flags)
}
end
def explode_extglob(pattern)
if match=pattern.match(/\{([^{}]+)}/) then
subpatterns = match[1].split(',',-1)
subpatterns.map{|subpattern| explode_extglob(match.pre_match+subpattern+match.post_match)}.flatten
else
[pattern]
end
end
end
end
Better testing is needed, but it seems to work fine for simple cases :
[2] pry(main)> File.explode_extglob('c{at,ub}s')
=> ["cats", "cubs"]
[3] pry(main)> File.explode_extglob('c{at,ub}{s,}')
=> ["cats", "cat", "cubs", "cub"]
[4] pry(main)> File.explode_extglob('{a,b,c}{d,e,f}{g,h,i}')
=> ["adg", "adh", "adi", "aeg", "aeh", "aei", "afg", "afh", "afi", "bdg", "bdh", "bdi", "beg", "beh", "bei", "bfg", "bfh", "bfi", "cdg", "cdh", "cdi", "ceg", "ceh", "cei", "cfg", "cfh", "cfi"]
[5] pry(main)> File.explode_extglob('{a,b}c*')
=> ["ac*", "bc*"]
[6] pry(main)> File.fnmatch('c{at,ub}s', 'cats')
=> false
[7] pry(main)> File.fnmatch_extglob('c{at,ub}s', 'cats')
=> true
[8] pry(main)> File.fnmatch_extglob('c{at,ub}s*', 'catsssss')
=> true
Tested with Ruby 1.9.3 and Ruby 2.1.5 and 2.2.1.

Transposing a string

Given a (multiline) string, where each line is separated by "\n" and may not be necessarily of the same length, what is the best way to transpose it into another string as follows? Lines shorter than the longest one should be padded with space (right padding in terms of the original, or bottom padding in terms of the output). Applying the operation on a string twice should be idempotent modulo padding.
Input string
abc
def ghi
jk lm no
Output string
adj
bek
cf
l
gm
h
in
o
Here are five approaches. (Yes, I got a bit carried away, but I find that trying to think of different ways to accomplish the same task is good exercise for the grey cells.)
#1
An uninteresting, brute-force method:
a = str.split("\n")
l = a.max_by(&:size).size
puts a.map { |b| b.ljust(l).chars }
.transpose
.map { |c| c.join.rstrip }.join("\n")
adj
bek
cf
l
gm
h
in
o
#2
This method and all that follow avoid the use of ljust and transpose, and make use of the fact that if e is an empty array, e.shift returns nil and leaves e an empty array. (Aside: I am often reaching for the non-existent method String#shift. Here it would have avoided the need to convert each line to an array of characters.)
a = str.split("\n").map(&:chars)
a.max_by(&:size).size.times.map { a.map { |e| e.shift || ' ' }.join.rstrip }
#3
This and the remaining methods avoid the need to compute the length of the longest string:
a = str.split("\n").map(&:chars)
a_empty = Array(a.size, [])
[].tap { |b| b << a.map { |e| e.shift || ' ' }.join.rstrip while a != a_empty }
#4
This method makes use of Enumerator#lazy, which has been available since v2.0.
a = str.split("\n").map(&:chars)
(0..Float::INFINITY).lazy.map do |i|
a.each { |e| e.shift } if i > 0
a.map { |e| e.first || ' ' }.join.rstrip
end.take_while { c = a.any? { |e| !e.empty? } }.to_a
(I initially had a problem getting this to work, as I was not getting the element of the output (" o"). The fix was adding the third line and changing the line that follows from a.map { |e| e.shift || ' ' }.join.rstrip to what I have now. I mention this because it seems like it may be common problem when using lazy.)
#5
Lastly, use recursion:
def recurse(a, b=[])
return b[0..-2] if a.last.empty?
b << a.map { |e| e.shift || ' ' }.join.rstrip
recurse(a, b)
end
a = str.split("\n").map(&:chars)
recurse(a)
I would write it like this:
def transpose s
lines = s.split(?\n)
longest = lines.map { |l| l.length }.max
(0..longest).map do |index|
lines.map { |l| l[index] || ' ' }.join
end * ?\n
end
This one works
s = "abc\ndef ghi\njk lm no\n"
s = s.split("\n")
s2 = ''
i = 0
while true
line = ''
s.each do |row|
line += (row[i] or ' ')
end
if line.strip == ''
break
end
s2 += line + "\n"
i += 1
end
puts s2
This one also works
s = "abc\ndef ghi\njk lm no\n"
s = s.split("\n")
maxlen = s.inject(0) {|m,r| m=[m, r.length].max}
s.map! {|r| r.ljust(maxlen).split(//)}
s = s.transpose.map {|r| r.join('')}.join("\n")
puts s
A play on what Chron did for an earlier version of ruby (e.g., 1.8.x). Example based on your original input that showed newline characters
str="abc\\n
def ghi\\n
jk lm no\\n"
def transpose s
lines = s.gsub("\\n","").split("\n")
longest = lines.map { |line| line.length }.max
(0..longest).map do |char_index|
lines.map { |line| line.split('')[char_index] || ' ' }.join
end * "\\n\n"
end
puts transpose(str)
I would write it like this:
def transpose_text(text)
# split the text into lines
text = text.split("\n")
# find the length of the longest line
max_line_length = text.map(&:size).max
# pad each line with white space and convert them to character arrays
text.map! { |line| line.ljust(max_line_length).chars }
#transpose the character arrays and then join them all into one string
text.transpose.map(&:join).join("\n")
end

Inconsistent printing with Ruby

I am trying to print a ruby hash:
opts = {
'one' => '1',
'two' => '1',
'three' => '0'
}
I want the output to be
one=1
two=1
three=0
This works fine with this code on one machine which runs ruby 1.8.7
print opts.map{|k,v| k + '=' + v + "\n"}.to_s
But on a different machine which runs ruby 1.9, it prints
["one=1\n", "two=1\n", "three=0\n"]
What is going wrong?
Try
print opts.map{|k,v| k + '=' + v + "\n"}.join
The explanation is easy: With ruby 1.9 Array.to_s changed its behaviour.
An alternative:
puts opts.map{|k,v| k + '=' + v }.join("\n")
or
puts opts.map{|k,v| "#{k}=#{v}" }.join("\n")
I would prefer:
opts.each{|k,v| puts "#{k}=#{v}" }
And another version, but with another look:
opts.each{|k,v| puts "%-10s= %s" % [k,v]}
The result is:
one = 1
two = 1
three = 0
(But the keys should be not longer then the length in %-10s.)
It's working as expected. Give this a try:
a={:one=>1, :two=>2, :three=>3}
a.each {|k,v| puts "#{k}=>#{v}" }
Try:
res = ""
opts.map{|k,v| res += k + '=' + v + "\n"}
puts res

More concise way of inserting a character between all characters in a string

I want to insert a character at every possible index of a string, including before the first element and after the last. Right now I'm doing:
result = []
result << c + str
result << str + c
for i in 0..str.length-2 do
result << (str[0..i] + c + str[i+1..-1])
end
Is there a way of doing this without having 2 special cases and having a loop from 0 to str.length - 2
EDIT
Sample output with '-' and 'hello':
["-hello", "h-ello", "he-llo", "hel-lo", "hell-o", "hello-"]
I'll assume you want ["-hello", "h-ello", "he-llo", "hel-lo", "hell-o", "hello-"], your question is not clear.
s = "hello"
(0..s.size).map { |i| s.clone.insert(i, "-") }
#=> ["-hello", "h-ello", "he-llo", "hel-lo", "hell-o", "hello-"]
For those that prefer a functional approach (I do):
(0..s.size).map { |i| (s[0...i] + "-" + s[i..-1]) }
#=> ["-hello", "h-ello", "he-llo", "hel-lo", "hell-o", "hello-"]

What is the syntax for array.select?

I'm trying to use Array.select to separate out, and then delete, strings from a database that contain unwanted items. I get no errors but this does not seem to be working as hoped.
The relevant code is the last part:
totaltext = []
masterfacs = ''
nilfacs = ''
roomfacs_hash = {'lcd' => lcd2, 'wifi'=> wifi2, 'wired' => wired2, 'ac' => ac2}
roomfacs_hash.each do |fac, fac_array|
if roomfacs.include? (fac)
totaltext = (totaltext + fac_array)
masterfacs = (masterfacs + fac + ' ')
else
nilfacs = (nilfacs + fac + ' ')
end
end
finaltext = Array.new
text_to_delete = totaltext2.select {|sentences| sentences =~ /#{nilfacs}/i}
finaltext = totaltext2.delete (text_to_delete)
puts finaltext
It's probably not working because delete isn't a chainable method (the return value is the object you are trying to delete on success, or nil if not found; not the modified array). To simplify your code, just use reject
finaltext = totaltext.reject{|sentence| nilfacs.any?{|fac| sentence =~ /#{fac}/i } }

Resources