Functional approach to joining an Array of values with varying separator? - ruby

I'm looking for a functional way to perform this messy procedural logic:
values = [a, b, c, d, e, f]
last_value = nil
string = ""
values.each do |v|
string << if last_value && last_value.special?
"/x/" + v.name.to_s
else
"/" + v.name.to_s
end
last_value = v
end
I basically have an array of objects (all the same type) and need to join their #name attributes, but following an object that has a particular characteristic, I need a different separator.
This is an easily solved problem, but I'm looking for the cleanest, most functional approach. I first dived into #inject, but you lose the previous value at each iteration, so I couldn't make that work. Any ideas?
I'd love to post the real code instead of pseudo code, but it's really dense and complex DataMapper Relationship stuff, so you probably couldn't just run it anyway, sorry :(

If I understand correctly what you want, this should work:
output = values.map do |v|
["/" + v.name.to_s, v.special? ? "/x" : ""]
end.flatten[0...-1].join
Alternative phrasing (Ruby 1.9):
output = "/" + values.flat_map do |v|
[v.name.to_s, ("x" if v.special?)]
end.take(2*values.size - 1).join("/")
Without analyzing the algorithm, just making it functional:
output = ([nil] + values).each_cons(2).map do |last_value, v|
if last_value && last_value.special?
"/x/" + v.name.to_s
else
"/" + v.name.to_s
end
end.join

try values.collect{|v| v.special? ? v + "/x/" : v + "/"}.join("")
EDIT, solution using inject:
values.inject(["", ""]) {|path_and_sep, item| [path_and_sep[0] + path_and_sep[1] + item, item.special? "/x/" : "/"]} [0]

Do the join at the end to get rid of the leading and trailing /:
values.collect{|v| v.special? && v != values.last ? [v.name.to_s, "x"] : v.name.to_s}.flatten.join("/")

I'm not particularly proud of that one, but it's kind of functional ;)
values.clone.unshift(nil).each_cons(2).map { |last_value, v|
last_value.special? ? "/x/" + v.to_s : "/" + v.to_s
}.join()
The clone is needed because each_cons destroys he original array.

values.map{|x| x.special? ? [x, SEPARATOR_2] : [x, SEPARATOR_1]}.flatten[0..-2].join('')

values.inject('') { |m, e| m << e.to_s; m << (e.special? ? '/x/' : '/' }
To make it completely functional you could recreate m each time instead of appending to it.
And to avoid the last iteration, perhaps something more complex like:
delay = ''
values.inject('') do |m, e|
m << delay << e.to_s
delay = e.special? ? '/x/' : '/'
m
end

Related

Stringifying query parameters: can we go further?

I wrote this method:
def stringify_query_params(query_parameters)
stringified_query_params = ''
query_parameters.each_with_index do |kv, i|
k, v = kv
index = i
if index == 0
stringified_query_params += "?#{k}=#{v}"
else
stringified_query_params += "&#{k}=#{v}"
end
end
return stringified_query_params
end
RubyCop is complaining in my running instance of RubyMine saying that I should instead be capturing the output of the conditional branching logic like this.
I was able to make it slightly better, using some methods in the Enumerable module
def stringify_query_parameters(query_parameters)
query_parameters.each_with_object([]).with_index do |((k, v), acc), index|
acc.push((index.positive? ? '&' : '?') + "#{k}=#{v}")
end.join('')
end
Can anyone think of a way to make it even terser?
It can be as follows:
def stringify_query_parameters(query_parameters)
'?' + query_parameters.map{ |k, v| "#{k}=#{v}" }.join('&')
end

changing integers into words ruby without gems

I am trying to change numbers up to 100 from integers into words, but have run into some trouble, can anyone point out what is missing with my code:
def in_words(integer)
numWords = {
0=>"zero",
1=>"one",
2=>"two",
3=>"three",
4=>"four",
5=>"five",
6=>"six",
7=>"seven",
8=>"eight",
9=>"nine",
10=>"ten",
11=>"eleven",
12=>"twelve",
13=>"thirteen",
14=>"fourteen",
15=>"fifteen",
16=>"sixteen",
17=>"seventeen",
18=>"eighteen",
19=>"nineteen",
20=>"twenty",
30=>"thirty",
40=>"fourty",
50=>"fifty",
60=>"sixty",
70=>"seventy",
80=>"eighty",
90=>"ninety",
100=>"one hundred"
}
array = integer.to_s.split('')
new_array = []
numWords.each do |k,v|
array.each do |x|
if x = k
new_array.push(v)
end
end
end
new_array.join('')
end
Right now when I do:
inwords(0)
I get the following:
=>"zeroonetwothreefourfivesixseveneightnineteneleventwelvethirteenfourteenfiftee nsixteenseventeeneighteennineteentwentythirtyfourtyfiftysixtyseventyeightyninetyone hundred"
Edit
I noticed your code iterates through the array a lot of times and uses the = instead of the == in your if statements.
Your code could be more efficient using the Hash's #[] method in combination with the #map method.., here's a one-line alternative:
integer.to_s.split('').map {|i| numWords[i.to_i]} .join ' '
Also, notice that the integer.to_s.split('') will split the array into one-digit strings, so having numbers up to a hundred isn't relevant for the code I proposed.
To use all the numbers in the Hash, you might want to use a Regexp to identify the numbers you have. One way is to do the following (I write it in one line, but it's easy to break it down using variable names for each step):
integer.to_s.gsub(/(\d0)|([1]?\d)/) {|v| v + " "} .split.map {|i| numWords[i.to_i]} .join ' '
# or:
integer.to_s.gsub(/(#{numWords.keys.reverse.join('|')})/) {|v| v + " "} .split.map {|i| numWords[i.to_i]} .join ' '
# out = integer.to_s
# out = out.gsub(/(#{numWords.keys.reverse.join('|')})/) {|v| v + " "}
# out = out.split
# out = out.map {|i| numWords[i.to_i]}
# out = out.join ' '
Edit 2
Since you now mention that you want the method to accept numbers up to a hundred and return the actual number (23 => twenty three), maybe a different approach should be taken... I would recommend that you update your question as well.
def in_words(integer)
numWords = {
0=>"zero",
1=>"one",
2=>"two",
3=>"three",
4=>"four",
5=>"five",
6=>"six",
7=>"seven",
8=>"eight",
9=>"nine",
10=>"ten",
11=>"eleven",
12=>"twelve",
13=>"thirteen",
14=>"fourteen",
15=>"fifteen",
16=>"sixteen",
17=>"seventeen",
18=>"eighteen",
19=>"nineteen",
20=>"twenty",
30=>"thirty",
40=>"fourty",
50=>"fifty",
60=>"sixty",
70=>"seventy",
80=>"eighty",
90=>"ninety",
100=>"one hundred"
}
raise "cannot accept such large numbers" if integer > 100
raise "cannot accept such small numbers" if integer < 0
return "one hundred" if integer == 100
if integer < 20 || integer %10 == 0
numWords[integer]
else
[numWords[integer / 10 * 10], numWords[integer % 10]].join ' '
end
end
the integer / 10 * 10 makes the number a round number (ten, twenty, etc') because integers don't have fractions (so, 23/10 == 2 and 2 * 10 == 20). The same could be achieved using integer.round(-1), which is probably better.
It seems like all you're trying to do is find a mapping from an implicit hash
module NumWords
INT2STR = {
0=>"zero",
1=>"one",
2=>"two",
3=>"three",
4=>"four",
5=>"five",
6=>"six",
7=>"seven",
8=>"eight",
9=>"nine",
10=>"ten",
11=>"eleven",
12=>"twelve",
13=>"thirteen",
14=>"fourteen",
15=>"fifteen",
16=>"sixteen",
17=>"seventeen",
18=>"eighteen",
19=>"nineteen",
20=>"twenty",
30=>"thirty",
40=>"fourty",
50=>"fifty",
60=>"sixty",
70=>"seventy",
80=>"eighty",
90=>"ninety",
100=>"one hundred"
}
module_function
def in_words(integer)
INT2STR[integer]
end
end
The above code separates the hash definition from the method call so that the hash doesn't get recreated every time you call in_words.
You can also use Hash#fetch instead of Hash#[] as Andrey pointed out.
Your test whether x = k is your first problem (in two ways).
Firstly, if x = k means assign the value of k to x and then execute the if block if that value is true (basically anything other than false or nil).
What you should actually be testing is x == k which will return true if x is equal to k.
The second problem is that you converted your number into an array of string representation so you are comparing, for example, if "0" == 0. This won't return true because they are different types.
If you convert it to if x.to_i == k then your if block will be executed and you'll get:
> in_words(0)
=> "zero"
Then you get to move onto the next problem which is that you're looking at your number digit by digit and some of the values you are testing against need two digits to be recognised:
> in_words(10)
=> "zeroone"
You might be in looking at a different question then - or maybe that is the question you wanted answered all along!
Here's another way you might do it:
ONES_TO_TEXT = { 0=>"zero", 1=>"one", 2=>"two", 3=>"three", 4=>"four",
5=>"five", 6=>"six", 7=>"seven", 8=>"eight", 9=>"nine" }
TEENS_TO_TEXT = { 10=>"ten", 11=>"eleven", 12=>"twelve",
13=>"thirteen", 15=>"fifteen" }
TENS_TO_TEXT = { 2=>"twenty", 3=>"thirty", 5=>"fifty", 8=>"eighty" }
def in_words(n)
raise ArgumentError, "#{n} is out-of_range" unless (0..100).cover?(n)
case n.to_s.size
when 1 then ONES_TO_TEXT[n]
when 3 then "one hundred"
else
case n
when (10..19)
TEENS_TO_TEXT.key?(n) ? TEENS_TO_TEXT[n] : ONES_TO_TEXT[n]+"teen"
else
t,o = n.divmod(10)
(TENS_TO_TEXT.key?(t) ? TENS_TO_TEXT[t] : ONES_TO_TEXT[t]+"ty") +
(o.zero? ? '' : "-#{ONES_TO_TEXT[o]}")
end
end
end
Let's try it:
in_words(5) #=> "five"
in_words(10) #=> "ten"
in_words(15) #=> "fifteen"
in_words(20) #=> "twenty"
in_words(22) #=> "twenty-two"
in_words(30) #=> "thirty"
in_words(40) #=> "fourty"
in_words(45) #=> "fourty-five"
in_words(50) #=> "fifty"
in_words(80) #=> "eighty"
in_words(99) #=> "ninety-nine"
in_words(100) #=> "one hundred"
Here the increased complexity may not be justified, but this approach may in fact simplify the calculations when the maximum permitted value of n is much greater than 100.

Compare two dimensional arrays

I have two two-dimensional arrays,
a = [[17360, "Z51.89"],
[17361, "S93.601A"],
[17362, "H66.91"],
[17363, "H25.12"],
[17364, "Z01.01"],
[17365, "Z00.121"],
[17366, "Z00.129"],
[17367, "K57.90"],
[17368, "I63.9"]]
and
b = [[17360, "I87.2"],
[17361, "s93.601"],
[17362, "h66.91"],
[17363, "h25.12"],
[17364, "Z51.89"],
[17365, "z00.121"],
[17366, "z00.129"],
[17367, "k55.9"],
[17368, "I63.9"]]
I would like to count similar rows in both the arrays irrespective of the character case, i.e., "h25.12" would be equal to "H25.12".
I tried,
count = a.count - (a - b).count
But (a - b) returns
[[17360, "Z51.89"],
[17361, "S93.601A"],
[17362, "H66.91"],
[17363, "H25.12"],
[17364, "Z01.01"],
[17365, "Z00.121"],
[17366, "Z00.129"],
[17367, "K57.90"]]
I need the count as 5 since there are five similar rows when we do not consider the character case.
Instead of a - b you should do this:
a.map{|k,v| [k,v.downcase]} - b.map{|k,v| [k,v.downcase]} # case-insensitive
You can convert Arrays to Hash, and use Enumerable#count with a block.
b_hash = b.to_h
a.to_h.count {|k, v| b_hash[k] && b_hash[k].downcase == v.downcase }
# => 5
It will convert second element of inner array to upcase for both array then you can perform subtraction, then It will return exact result that you want
a.map{|first,second| [first,second.upcase]} - b.map{|first,second| [first,second.upcase]}
You can zip them and then use the block form of count:
a.zip(b).count{|e| e[0][1].downcase == e[1][1].downcase}
a.count - (a.map{|e| [e[0],e[1].downcase] } - b.map{|e| [e[0],e[1].downcase] }).count
The above maps a and b to new arrays where the second sub-array element is downcase.
You want to count similar, so &(AND) operation is more suitable.
(a.map { |k, v| [k, v.upcase] } & b.map { |k, v| [k, v.upcase] }).count
Using Proc and '&':
procedure = Proc.new { |i, j| [i, j.upcase] }
(a.map(&procedure) & b.map(&procedure)).count
#=> 5
For better understanding, let's simplify it:
new_a = a.map {|i, j| [i, j.upcase]}
new_b = b.map {|i, j| [i, j.upcase]}
# Set intersection using '&'
(new_a & new_b).count
#=> 5
I have assumed that the ith element of a is to be compared with the ith element of b. (Edit: a subsequent comment by the OP confirmed this interpretation.)
I would be inclined to use indices to avoid the construction of relatively large temporary arrays. Here are two ways that might be done.
#1 Use indices
[a.size,b.size].min.size.times.count do |i|
af,al=a[i]
bf,bl=b[i];
af==bf && al.downcase==bl.downcase
end
#=> 5
#2 Use Refinements
My purpose in giving this solution is to illustrate the use of Refinements. I would not argue for its use for the problem at hand, but this problem provides a good vehicle for showing how the technique can be applied.
I could not figure out how best to do this, so I posted this question on SO. I've applied #ZackAnderson's answer below.
module M
refine String do
alias :dbl_eql :==
def ==(other)
downcase.dbl_eql(other.downcase)
end
end
refine Array do
def ==(other)
zip(other).all? {|x, y| x == y}
end
end
end
'a' == 'A' #=> false (as expected)
[1,'a'] == [1,'A'] #=> false (as expected)
using M
'a' == 'A' #=> true
[1,'a'] == [1,'A'] #=> true
I could use Enumerable#zip, but for variety I'll use Object#to_enum and Kernel#loop in conjunction with Enumerator#next:
ea, eb = a.to_enum, b.to_enum
cnt = 0
loop do
cnt += 1 if ea.next == eb.next
end
cnt #=> 5

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-"]

mapping elements of an array differently based on position

I want to map elements of an array such that all elements
of the array are floats, except the first element which
is a string.
Anyone know how I can do this?
Tried this but doesn't work:
arr = arr.map { |e| e.to_i if e != arr.first }
Another solution is
[array.first] + array.drop(1).map &:to_f
This makes it clear that you want the first element separate from the rest, and you want the rest of the elements to be of type Float. Other options include
array.map { |element, index| index == 0 ? element : element.to_f }
array.map { |element| element == array.first ? element : element.to_f }
You can use a short ternary expression here:
a.map { |e| ( e == a.first ) ? e : e.to_f }
Another option (if you don't want to use ternary operators) is to do the following:
arr = arr.map { |e| (e == arr.first) && e || e.to_f}
This alternative is discussed here. A limitation with this method is that the first element in the array cannot be nil (or some other value that would evaluate false in a boolean evaluation), because if so, it will evaluate to the || expression and return e.to_f instead of just e.
Ruby 1.9 only?
arr = arr.map.with_index { |e, i| i.zero? ? e.to_s : e.to_f }
You can ask the objects themselves whether they're numbers.
"column heading".respond_to?(:to_int) # => false
3.1415926.respond_to?(:to_int) # => true
new_arr = arr.map do |string_or_float|
if string_or_float.respond_to?(:to_int)
string_or_float.to_int # Change from a float into an integer
else
string_or_float # Leave the string as-is
end
end
respond_to?(:to_int) means "Can I call to_int on you?"
to_int is a method that only objects that are readily convertable to integers should have. Unlike to_i, which is "I'm not very much like an integer, but you can try to convert me into a integer", to_int means "I'm very much like an integer - convert me into an integer with full confidence!"

Resources