Help refactoring this nasty Ruby if/else statement - ruby-on-rails

So I have this big, hairy if/else statement. I pass a tracking number to it, and then it determines what type of tracking number it is.
How can I simplify this thing? Specifically wanting to reduce the number of lines of codes.
if num_length < 8
tracking_service = false
else
if number[1, 1] == 'Z'
tracking_service = 'ups'
elsif number[0, 1] == 'Q'
tracking_service = 'dhl'
elsif number[0, 2] == '96' && num_length == 22
tracking_service = 'fedex'
elsif number[0, 1] == 'H' && num_length == 11
tracking_service = 'ups'
elsif number[0, 1] == 'K' && num_length == 11
tracking_service = 'ups'
elsif num_length == 18 || num_length == 20
check_response(number)
else
case num_length
when 17
tracking_service = 'dhlgm'
when 13,20,22,30
tracking_service = 'usps'
when 12,15,19
tracking_service = 'fedex'
when 10,11
tracking_service = 'dhl'
else
tracking_service = false
end
end
end
Yes, I know. It's nasty.

Try this. I rewrote it using case and regular expressions. I also used :symbols instead of "strings" for the return values, but you can change that back.
tracking_service = case number
when /^.Z/ then :ups
when /^Q/ then :dhl
when /^96.{20}$/ then :fedex
when /^[HK].{10}$/ then :ups
else
check_response(number) if num_length == 18 || num_length == 20
case num_length
when 17 then :dhlgm
when 13, 20, 22, 30 then :usps
when 12, 15, 19 then :fedex
when 10, 11 then :dhl
else false
end
end

Depending on whether or not the tracking code is a ruby object, you could also put helper's in it's class definition:
class TrackingCode < String
# not sure if this makes sense for your use case
def ups?
self[1,1] == 'Z'
end
def dhl?
self[0,1] == 'Q'
end
def fedex?
self.length == 22 && self[0, 2] == '96'
end
# etc...
end
Then your conditional becomes:
if number.ups?
# ...
elsif number.dhl?
# ...
elseif number.fedex?
end
One simplified conditional where you are operating on the implied feature of the tracking code. Likewise, if you were to take a looping approach, your loop would also be cleaner:
%w(ups? dhl? fedex?).each do |is_code|
return if number.send(is_code)
end
or even:
%w(ups? dhl? fedex?).each do |is_code|
yield if number.send(is_code)
end

This method looks like it was written for speed. You can use a minhash as a substitute, but I think the code is fairly clean and doesn't require a refactor. Rubyists tend to be disgusted by needless structure, but oftentimes it's needed to model real-world situations and/or provides a performance boost. The keyword should be needless.

Whilst longer than jtbandes solution, you might like this as it's a bit more declarative:
class Condition
attr_reader :service_name, :predicate
def initialize(service_name, &block)
#service_name = service_name
#predicate = block
end
end
CONDITIONS = [
Condition.new('ups') { |n| n[1] == 'Z' },
Condition.new('dhl') { |n| n[0] == 'Q' },
Condition.new('fedex') { |n| n[0..1] == '96' && n.size == 22 },
Condition.new('ups') { |n| n[0] == 'H' && n.size == 11 },
Condition.new('ups') { |n| n[0] == 'K' && n.size == 11 },
Condition.new('dhlgm') { |n| n.size == 17 },
Condition.new('usps') { |n| [13, 20, 22, 30].include?(n.size) },
Condition.new('fedex') { |n| [12, 15, 19].include?(n.size) },
Condition.new('dhl') { |n| [10, 11].include?(n.size) },
]
def tracking_service(tracking_number)
result = CONDITIONS.find do |condition|
condition.predicate.call(tracking_number)
end
result.service_name if result
end
I haven't dealt with the check_response method call here as I feel you should probably handle that elsewhere (assuming it does something other than return a tracking service name).

I believe this is sufficiently complex to deserve its own method.
BTW, if the length is 20 then the original function returns whatever check_response(n) returns, yet then attempts (and will always fail) to return 'usps'.
#lenMap = Hash.new false
#lenMap[17] = 'dhlgm'
#lenMap[13] = #lenMap[20] = #lenMap[22] = #lenMap[30] = 'usps'
#lenMap[12] = #lenMap[15] = #lenMap[19] = 'fedex'
#lenMap[10] = #lenMap[11] = 'dhl'
def ts n
len = n.length
return false if len < 8
case n
when /^.Z/
return 'ups'
when /^Q/
return 'dhl'
when /^96....................$/
return 'fedex'
when /^[HK]..........$/
return 'ups'
end
return check_response n if len == 18 or len == 20
return #lenMap[len]
end
# test code...
def check_response n
return 'check 18/20 '
end
%w{ 1Zwhatever Qetcetcetc 9634567890123456789012 H2345678901
K2345678901 hownowhownowhownow hownowhownowhownow90
12345678901234567
1234567890123
12345678901234567890
1234567890123456789012
123456789012345678901234567890
123456789012
123456789012345
1234567890123456789
1234567890
12345678901 }.each do |s|
puts "%32s %s" % [s, (ts s).to_s]
end

Related

Opposite of Ruby's number_to_human

Looking to work with a dataset of strings that store money amounts in these formats. For example:
$217.3M
$1.6B
$34M
€1M
€2.8B
I looked at the money gem but it doesn't look like it handles the "M, B, k"'s back to numbers. Looking for a gem that does do that so I can convert exchange rates and compare quantities. I need the opposite of the number_to_human method.
I would start with something like this:
MULTIPLIERS = { 'k' => 10**3, 'm' => 10**6, 'b' => 10**9 }
def human_to_number(human)
number = human[/(\d+\.?)+/].to_f
factor = human[/\w$/].try(:downcase)
number * MULTIPLIERS.fetch(factor, 1)
end
human_to_number('$217.3M') #=> 217300000.0
human_to_number('$1.6B') #=> 1600000000.0
human_to_number('$34M') #=> 34000000.0
human_to_number('€1M') #=> 1000000.0
human_to_number('€2.8B') #=> 2800000000.0
human_to_number('1000') #=> 1000.0
human_to_number('10.88') #=> 10.88
I decided to not be lazy and actually write my own function if anyone else wants this:
def text_to_money(text)
returnarray = []
if (text.count('k') >= 1 || text.count('K') >= 1)
multiplier = 1000
elsif (text.count('M') >= 1 || text.count('m') >= 1)
multiplier = 1000000
elsif (text.count('B') >= 1 || text.count('b') >= 1)
multiplier = 1000000000
else
multiplier = 1
end
num = text.to_s.gsub(/[$,]/,'').to_f
total = num * multiplier
returnarray << [text[0], total]
return returnarray
end
Thanks for the help!

How to use .map in Rails

Pig Latin
Rule 1: If a word begins with a vowel sound, add an "ay" sound to
the end of the word.
Rule 2: If a word begins with a consonant sound, move it to the end
of the word, and then add an "ay" sound to the end of the word.
The following program works in ruby. But I'm confused on how to use the "map" function? Please see the code as follows:
def translate(sentence)
if sentence.include?(" ")
words = sentence.split(" ").map do |word|
translate_word(word)
end
return words.join(" ")
else single_word = sentence
translate_word(single_word)
end
end
The above sentences works! but if I use:
words = sentence.split(" ")
words.map do |word|
translate_word(word)
end
It DOESN'T work! Why? I thought they were the same...
def translate_word(w)
vowels = %w[a e i o u]
consonants = ("a".."z").to_a - vowels
if vowels.include?(w[0])
w + "ay"
elsif consonants.include?(w[0]) && vowels.include?(w[1]) && w[1] != "u"
w[1..-1] + w[0] + "ay"
elsif (consonants.include?(w[0]) && consonants.include?(w[1]) && vowels.include?(w[2]) && w[2] != "u") || (w[0] == "q" && w[1] == "u")
w[2..-1] + w[0..1] + "ay"
elsif (consonants.include?(w[0]) && consonants.include?(w[1]) && consonants.include?(w[2]) && vowels.include?(w[3]))
w[3..-1] + w[0..2] + "ay"
elsif consonants.include?(w[0]) && w[1] == "q" && w[2] == "u"
w[3..-1] + w[0..2] + "ay"
end
end
#map function returns a new object which you are dismissing.
To save the result you should assign it back to words like this:
words = sentence.split(" ")
words = words.map do |word|
translate_word(word)
end
Or use #map! instead.

longest palindrome in Ruby on Rails

Write a method that takes in a string of lowercase letters (no uppercase letters, no repeats). Consider the substrings of the string: consecutive sequences of letters contained inside the string.
Find the longest such string of letters that is a palindrome.
Based on local method Palindrome?(string), I implemented longest-palindrome(string) as below with test cases:
def palindrome?(string)
i = 0
while i < string.length
if string[i] != string[(string.length - 1) - i]
return false
end
i += 1
end
return true
end
def longest_palindrome(string)
dix = 0
lstr = ""
lstrc = nil
while dix < string.length
dix2 = 1
while dix2 < string.length
str = string.slice(dix,dix2)
count = str.length
if palindrome?(str)
if lstrc == nil || lstrc < count
lstr = str
lstrc = count
end
end
dix2 += 1
end
dix += 1
end
puts(lstr)
return lstr
end
# These are tests to check that your code is working. After writing
# your solution, they should all print true.
puts(
'longest_palindrome("abcbd") == "bcb": ' +
(longest_palindrome('abcbd') == 'bcb').to_s
)
puts(
'longest_palindrome("abba") == "abba": ' +
(longest_palindrome('abba') == 'abba').to_s
)
puts(
'longest_palindrome("abcbdeffe") == "effe": ' +
(longest_palindrome('abcbdeffe') == 'effe').to_s
)
Test results as below:
bcb
longest_palindrome("abcbd") == "bcb": true
bb
longest_palindrome("abba") == "abba": false
effe
longest_palindrome("abcbdeffe") == "effe": true
Why did the second test failed?
... this line is preventing you from considering the entire string
while dix2 < string.length
So when dix is the whole string, you're not doing any testing for palindromes
Change the line to...
while dix2 <= string.length
It would actually be slightly more efficient if you did...
while dix2 <= string.length - dix
Which would prevent you from testing (for, say, a string of length 10), string(7,3) and string(7,4) and string(7,5) etc. etc., which are all basically the same string.

Can I iterate through an array during a comparison?

s = Array.new
s << 19
while (s.last + 19) < 100000 do
s << s.last + 19
end
This^ works. s is an array of all factors of 19 below 100,000.
I'm trying to, in a succinct statement, find all numbers in s where the reverse of that number is also in the array. Ex: 176 and 671.
reflections= s.select { |num| num.to_s.reverse == s.each.to_s }
I know this is wrong, but how can I check each reversed item against the entire array?
This should work:
reflections = s.select { |num| s.include?(num.to_s.reverse.to_i) }
Although it produces results that you probably didn't anticipate
s = [176, 234, 671, 111]
reflections = s.select { |num| s.include?(num.to_s.reverse.to_i) }
reflections # => [176, 671, 111]
These are all valid results according to your logic.
Excluding self-match is pretty straighforward:
s = [176, 234, 671, 111]
reflections = s.select do |x|
x = x.to_s
r = x.reverse
(x != r) && s.include?(r.to_i)
end
reflections # => [176, 671]
reflections = s & s.map{|num| num.to_s.reverse.to_i}
Try:
reverse_array = s.select {|num| num.to_s == num.to_s.reverse }
UPDATE:
After checking I found this will work:
myarr = ""
s = (1..1000)
s.select{ |num|
unless s.include?(num.to_s.reverse.to_i)
myarr << num.to_s
end
}
Finally, the myarr will contain all the numbers whose reverse is present in array s.

all values same sign validation

User should insert all the values either positive or negative.
How may i set same sign validation ?
Right i have written this on before_save ..
unless (self.alt_1 >= 0 && self.alt_2 >=0 && self.alt_3 >= 0 &&
self.alt_4 >= 0 && self.alt_5 >= 0 && self.alt_6 >= 0) ||
(self.alt_1 <= 0 && self.alt_2 <=0 && self.alt_3 <= 0 &&
self.alt_4 <= 0 && self.alt_5 <= 0 && self.alt_6 <= 0)
self.errors.add_to_base(_("All values sign should be same."))
end
first_sign = self.alt_1 <=> 0
(2..6).each do |n|
unless (self.send("alt_#{n}") <=> 0) == first_sign
errors.add_to_base(_("All values' signs should be same."))
break
end
end
With this method we first get the sign of alt_1, and then see if the signs of the rest of the elements (alt_2 through alt_6) match. As soon as we find one that doesn't match we add the validation error and stop. It will run a maximum of 6 iterations and a minimum of 2.
Another more clever, but less efficient method, is to use the handy method Enumerable#all?, which returns true if the block passed to it returns true for all elements:
range = 1..6
errors.add_to_base(_("All values' signs should be same.")) unless
range.all? {|n| self.send("alt_#{n}") >= 0 } ||
range.all? {|n| self.send("alt_#{n}") <= 0 }
Here we first check if all of the elements are greater than 0 and then if all of the elements are less than 0. This method iterates a maximum of 12 times and a minimum of 6.
Here's a slightly different approach for you:
irb(main):020:0> def all_same_sign?(ary)
irb(main):021:1> ary.map { |x| x <=> 0 }.each_cons(2).all? { |x| x[0] == x[1] }
irb(main):022:1> end
=> nil
irb(main):023:0> all_same_sign? [1,2,3]
=> true
irb(main):024:0> all_same_sign? [1,2,0]
=> false
irb(main):025:0> all_same_sign? [-1, -5]
=> true
We use the spaceship operator to obtain the sign of each number, and we make sure that each element has the same sign as the element following it. You could also rewrite it to be more lazy by doing
ary.each_cons(2).all? { |x| (x[0] <=> 0) == (x[1] <=> 0) }
but that's less readable in my opinion.
unless
[:<=, :>=].any? do |check|
# Check either <= or >= for all values
[self.alt1, self.alt2, self.alt3, self.alt4, self.alt5, self.alt6].all? do |v|
v.send(check, 0)
end
end
self.errors.add_to_base(_("All values sign should be same."))
end

Resources