I have some ranges defined:
very_low = <10
low = 11-20
medium = 21-30
high = 31-40
very_high = 41+
What is the most succinct way in Ruby to find out which category any given number is in?
e.g know that 23 would be 'medium'
I would do:
RANGES = {
very_low: (..10),
low: (11..20),
medium: (21..30),
high: (31..40),
very_high: (41..)
}
def range_for(number)
RANGES.find { |_, range| range.cover?(number) }.first
end
range_for(23)
#=> :medium
It uses Enumerable#find with Range#cover? to pick the matching range definition. The find method returns an array including both, key and value. The chained first call then picks the key from the array which will be the requested range identifier.
you've changed your question, but you now have overlaps (40, and 40+) ... I don't know if you're working with integers or decimals, but I'm assuming integers because you're not dealing with "10" or "10.5" ... how about this:
def which_range? num
ranges={
very_low: (..10),
low: (11..20),
medium: (21..30),
high: (31..40),
very_high: (41..)
}
ranges.each do |key,val|
return key if val.include?(num)
end
return false
end
example output:
2.7.2 :073 > which_range? 23
=> :medium
2.7.2 :074 > which_range? 7
=> :very_low
2.7.2 :075 > which_range? 25
=> :medium
2.7.2 :076 > which_range? 35
=> :high
2.7.2 :077 > which_range? 45
=> :very_high
2.7.2 :123 > which_range? 102
=> :very_high
2.7.2 :124 > which_range? -12
=> :very_low
If I get your question correctly, then, perhaps, case statement is something you looking for
case some_number
when ..10 then 'very_low'
when 11..20 then 'low'
when 40.. then 'very_high'
...
end
# P.S.: for older versions without endless ranges:
case some_number
when -Float::INFINITY..10 then 'very_low'
when 11..20 then 'low'
when 40..Float::INFINITY then 'very_high'
...
end
Assuming the ranges are consecutive, you could define the upper or lower bounds in an array in order from low to high (when using upper bounds) or from high to low (when using lower bounds). Then have an array with their respective name at the same index.
def range_name(number)
range_names = ['very_low', 'low', 'medium', 'high', 'very_high' ]
upper_bounds = [ 10 , 20 , 30 , 40 , Float::INFINITY] # inclusive
index = upper_bounds.find_index { |upper_bound| number <= upper_bound }
range_names[index]
end
Related
I'm trying to solve a problem :
Your task is to write a simple function that takes a number of meters,
and outputs it using metric prefixes.For this exercise we just want
units bigger than a meter, from meters up to yottameters, excluding
decameters and hectometers.All values passed in will be positive
integers
Examples
meters(51500)
# returns "51.5km"
meters(5000000)
# returns "5Mm"
My code:
def meters(x)
map_prefix={ 24=>'Y', 21=> 'Z', 18=> 'E', 15=> 'P', 12=> 'T', 9=>'G', 6=>'M', 3=>'k',0=>'' }
digits=(x.to_i.to_s.size-1)/3
division=x/(10.0**(3*digits))
"#{division}#{map_prefix[3*digits]}m".sub(/\.0([^\d])/,'\1')
end
It doesn't work for meters(56*10**24) #->expected 56Ym ,instead got 56.000000000004Ym, but it works for bigger numbers such as meters(88*10**24) #->88Ym. The code passes 49 out of 50 tests, can someone help me find the error?
The easiest way to hack your code to get it working seems to avoid float-pointing number, like here:
#!/usr/bin/env ruby
def meters(x)
map_prefix={ 24=>'Y', 21=> 'Z', 18=> 'E', 15=> 'P', 12=> 'T', 9=>'G', 6=>'M', 3=>'k',0$
map_prefix.default = 'Y'
digits = [((x.to_s.size-1)/3)*3, 24].min
division = x.to_s.insert(-digits - 1, '.')
division.sub!(/0+\z/, '')
division.sub!(/\.\z/, '')
"#{division}#{map_prefix[digits]}m"
end
puts meters(51500)
puts meters(5000000)
puts meters(5001)
puts meters(88*10**24)
puts meters(88*10**24 + 1)
puts meters(100)
puts meters(88*10**27)
puts meters(88*10**27 + 1)
With results like:
./ruby.rb
51.5km
5Mm
5.001km
88Ym
88.000000000000000000000001Ym
100m
88000Ym
88000.000000000000000000000001Ym
More seriously, you need to avoid strings whatsoever (no conversions to string at all should be made).
You need arbitrary precision, so float is not an option at all.
I think your issue is that you are multiplying by 10.0, yet you only want to deal with integers.
Something like the following is what you want. (I'm also doing a couple style changes).
def meters(x)
digits=(x.to_i.to_s.size-1)/3
prefix = prefixes[3*digits]
value = x / (10 ** (3 * digits))
"#{value}#{prefix}m".sub(/\.0([^\d])/,'\1')
end
def prefixes
{
24 => 'Y',
21 => 'Z',
18 => 'E',
15 => 'P',
12 => 'T',
9 => 'G',
6 => 'M',
3 => 'k',
0 => ''
}
end
This at least gives the correct solution to the one that is wrong. I'm not going to guarantee it is the correct solution to everything.
I made the hash into its own function here because it seems like it would be static. As well was the other things I mentioned in my comment.
You can use Float#round to round the number to certain digits. For this certain problem, 3 should do fine.
"#{division.round(3)}#{map_prefix[3*digits]}m".sub(/\.0([^\d])/,'\1')
# ^^^^^^^^^
The reason behind the problem is: Float can store integers that's pretty big. However, for integers bigger than a certain limit, Float cannot store them precisely. For [IEEE-754 double precision] floating point, that limit is 253.
FWIW this line will change ruby's duck typing to float. (notice you're introducing 10.0 as a float.)
division=x/(10.0**(3*digits))
When dealing with large numbers, its best to use the built in BigDecimal class. Much cleaner and less error prone, although this is definitely NOT error proof code.
require 'bigdecimal'
def meters(x)
b = BigDecimal.new(x).split
"#{b[1]}#{prefix[b[3] - b[1].length]}m"
end
def prefix
{
24 =>'Y', 21 => 'Z', 18 => 'E', 15 => 'P',
12 => 'T', 9 =>'G', 6 =>'M', 3 =>'k',0 =>''
}
end
UNITS = " kMGTPEZY"
def meters(x)
sx = x.to_s
pwr = sx.size - 1
return sx if pwr < 3
pfx_sz = (pwr < 24) ? (1 + pwr % 3) : pwr - 23
sxs = sx.reverse.to_i.to_s.reverse
sxs = sxs.ljust([sxs.size, pfx_sz].max, '0')
pfx = sxs[0, pfx_sz]
pfx << '.' if (pfx.size < sxs.size)
"#{ pfx }#{ sxs[pfx_sz..-1] }#{ UNITS[[pwr/3, 8].min] }"
end
meters 3 #=> "3"
meters 100 #=> "100"
meters 4000 #=> "4k"
meters 5001 #=> "5.001k"
meters 51500 #=> "51.5k"
meters 5000000 #=> "5M"
meters 88*10**24 #=> "88Y"
meters 88*10**24 + 1 #=> "88.000000000000000000000001Y"
meters 88*10**27 #=> "88000Y"
meters 88*10**27 + 1 #=> "88000.000000000000000000000001Y"
I have this code:
#counter = 719
#period_hash = {
:sunset => 360,
:day => 720,
:dawn => 1200,
}
#period = :nothing
def init_period
periods = #period_hash.keys
#period_hash.each_with_index do |(__, number), index|
if #counter < number
#period = periods[index - 1]
break
end
end
if #period == :nothing
#period = periods[-1]
end
end
init_period
p #period
I have a #counter which will have values between 0 and 1440.
Then I have a hash with variable content. The content will always be symbol => integer
The integer value will also be a number between 0 and 1440 and all numbers are unique in
the hash. The hash will be sorted, so the lowest number will be first and highest number
will be last.
Then I have a method(init_period) which will return the key corresponding to the #counter variable.
These are the intervals for the #counter and the returned symbol:
0 .. 359 => :dawn
360 .. 719 => :sunset
720 .. 1199 => :day
1200 .. 1440 => :dawn
It all works, but Im wondering if there are other and better ways to do the same.
You can achieve converting your hash to different structure with the following code. It is not very clear, as you are using information outside from #period_hash (like global range boundaries and the fact that last value must be reused).
hash = #period_hash.
keys.
unshift(#period_hash.keys.last). # reuse last key from hash
zip( # zip keys with ranges
[0, *#period_hash.values, 1440]. # add 0 and 1440 as boundaries
each_cons(2) # convert array to consecutive pairs enum
).each_with_object({}) {|(key, (from, to)), hash|
hash[from...to] = key # build actual hash
}
On this structure you can call code that will detect your period:
hash.detect {|a,b| a.include?(719) }
You end up with really unreadable code. If your periods are not going to change often you could go for very readable code:
def init_period(counter)
case counter
when 0...360 then :dawn
when 360...720 then :sunset
when 720...1200 then :day
when 1200...1440 then :dawn
end
end
This way period boundaries are not taken from external structure, but code is obvious and clear.
New to ruby. I have an array created by nokogiri like this :
Array = ["10:31 Main title", ...]
It is a schedule in the format hour:minute title.
Now I have a time, say 10:35 and I want to find the entry in the array with the nearest lower number (time and title). It is like what is playing now?
How can I do this in ruby? I am at a blank here...
Thank you
You can achieve this using bsearch like below
a = [1, 4, 8, 11, 97]
a.bsearch {|x| x >= 7} # which results 8
You're going to have to walk the array and parse each entry. You'll also have to take into consideration whether the times are 12-hour or 24-hour, e.g. "10:31 Main Title" does that mean 10:31 AM or PM (in 12 hour clock). If its a 24-hour clock then 10:31 is 10:31 [am] and you'll also have 22:31 to reflect 10:31 [pm].
So you could walk the array, parsing each entry and then building a new structure which you can sort by. Ultimately you can get the lowest value and then just find the index of that entry in the original array.
require 'date'
a1 = ["10:31 The Beastmaster", "10:36 C.H.U.D.", "11:30 Goonies", "11:30 Krull", "11:59 Batteries Not Included"]
#=> ["10:31 The Beastmaster", "10:36 C.H.U.D.", "11:30 Goonies", "11:30 Krull", "11:59 Batteries Not Included"]
h1 = {}; a1.each {|x| m = x.match(/(\d{1,2}:\d{2})\s+(\w.*)/); h1[m[1]] ||= []; h1[m[1]] << m[2]}; h1 # => hash with times as keys and array of titles as corresponding values
#=> {"10:31"=>["The Beastmaster"], "10:36"=>["C.H.U.D."], "11:30"=>["Goonies", "Krull"], "11:59"=>["Batteries Not Included"]}
t1 = DateTime.rfc3339('2014-02-03T10:35:00-08:00').to_time.to_i
#=> 1391452500
within_an_hour = 60 * 60
#=> 3600
t2 = t1 + within_an_hour
#=> 1391456100
a2 = h1.keys.partition {|x| x > Time.at(t1).strftime("%I:%M")}[0] # => all upcoming times
#=> ["10:36", "11:30", "11:59"]
h2 = {}; a2.each {|x| h2[x] = h1[x]}; h2 # => all upcoming show times with corresponding titles
#=> {"10:36"=>["C.H.U.D."], "11:30"=>["Goonies", "Krull"], "11:59"=>["Batteries Not Included"]}
a3 = a2.partition {|x| x < Time.at(t2).strftime("%I:%M")}[0] # => upcoming times within an hour
#=> ["10:36", "11:30"]
h3 = {}; a3.each {|x| h3[x] = h1[x]}; h3 # => upcoming show times with corresponding titles within an hour
#=> {"10:36"=>["C.H.U.D."], "11:30"=>["Goonies", "Krull"]}
using the above code in a method:
require 'date'
def what_is_playing_now(time, a1=["10:31 The Beastmaster", "10:36 C.H.U.D.", "11:30 Goonies", "11:30 Krull", "11:59 Batteries Not Included"])
h1 = {}; a1.each {|x| m = x.match(/(\d{1,2}:\d{2})\s+(\w.*)/); h1[m[1]] ||= []; h1[m[1]] << m[2]}; h1 # => hash with times as keys and array of titles as corresponding values
t1 = DateTime.rfc3339("2014-02-03T#{time}:00-08:00").to_time.to_i
a2 = h1.keys.partition {|x| x > Time.at(t1).strftime("%I:%M")}[0] # => all upcoming times
h2 = {}; a2.each {|x| h2[x] = h1[x]}; h2 # => all upcoming show times with corresponding titles
"#{a2.first} #{h2[a2.first].sort.first}"
end
what_is_playing_now("10:35")
#=> "10:36 C.H.U.D."
sources:
https://www.ruby-forum.com/topic/129755
http://ruby-doc.org/core-1.9.3/Enumerable.html#method-i-partition
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/date/rdoc/Date.html
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/date/rdoc/Time.html
Since your array contains strings starting with numbers, these can be nicely sorted naturally.
my_array.sort.reverse.find{ |i| i < "10:35" }
This will sort your array in ascending order, then reverse it, and finally return the first item for which the block returns true.
If you are at Ruby version > 2.0, you can also use Array#bsearch:
my_array.sort.bsearch{ |i| i < "10:35" }
This will sort your array and will then use a binary search approach to finding the desired item (Thanks #ala for pointing this out).
These simple lines of code expect the time to be in 24h format with leading zeros (i.e. hh:mm), since it depends on comparing the lines lexicographically.
My question is whether I can use a range as the value in a key:value pair in a hash. I am working on a problem where I am trying to return a letter grade (A-F) for an average of numerical grades (array of numbers). I have a working solution, but I came across something intriguing. Here is my code:
def get_grade(array)
avg = (array.inject {|num, x| num + x}) / array.length
grades = {
"A" => [90..10]
"B" => [80..89],
"C" => [70..79],
"D" => [60..69],
"F" => [0..59],
}
grades.default = "Error"
puts grades.key(avg)
end
arraya = [100,90,100,99,99]
puts get_grade(arraya)
I know I could return the letter grade with either a case or an if statement. It seems like I should be able to use a hash instead but it doesn't work. Why can't I set up a hash with a range as value? Thanks.
You could use a case statement:
def get_grade(scores)
case scores.inject(&:+) / scores.length
when 90..100; 'A'
when 80..89; 'B'
when 70..79; 'C'
when 60..69; 'D'
when 0..59; 'F'
else; 'Error'
end
end
arraya = [100,90,100,99,99]
puts get_grade(arraya)
#=> A
You may want to rewrite your method as the following:
def get_grade(array)
avg = array.inject(:+) / array.length
grades = {
"A" => (90..100),
"B" => (80..89),
"C" => (70..79),
"D" => (60..69),
"F" => (0..59),
}
grade = grades.find{|key, range| range.include?(avg) }
grade.nil? ? "Unknown" : grade.first
end
arraya = [100,90,100,99,99]
puts get_grade(arraya) # => A
Is there a built-in method in Ruby to support this?
if you are in Rails, you can convert 1 to 1st, 2 to 2nd, and so on, using ordinalize.
Example:
1.ordinalize # => "1st"
2.ordinalize # => "2nd"
3.ordinalize # => "3rd"
...
9.ordinalize # => "9th"
...
1000.ordinalize # => "1000th"
And if you want commas in large numbers:
number_with_delimiter(1000, :delimiter => ',') + 1000.ordinal # => "1,000th"
in ruby you do not have this method but you can add your own in Integer class like this.
class Integer
def ordinalize
case self%10
when 1
return "#{self}st"
when 2
return "#{self}nd"
when 3
return "#{self}rd"
else
return "#{self}th"
end
end
end
22.ordinalize #=> "22nd"
How about Linguistics? Its not built in though. If you want built in , you have to set it up using hashes etc..
See here also for examples
I wanted an ordinalize method that has "first, second, third" rather than '1st, 2nd, 3rd' - so here's a little snippet that works up to 10 (and falls back to the Rails ordinalize if it can't find it).
class TextOrdinalize
def initialize(value)
#value = value
end
def text_ordinalize
ordinalize_mapping[#value] || #value.ordinalize
end
private
def ordinalize_mapping
[nil, "first", "second", "third", "fourth", "fifth", "sixth", "seventh",
"eighth", "ninth", "tenth" ]
end
end
Here's how it works:
TextOrdinalize.new(1).text_ordinalize #=> 'first'
TextOrdinalize.new(2).text_ordinalize #=> 'second'
TextOrdinalize.new(0).text_ordinalize #=> '0st'
TextOrdinalize.new(100).text_ordinalize #=> '100th'
if you are not in Rails you could do
def ordinalize(n)
return "#{n}th" if (11..13).include?(n % 100)
case n%10
when 1; "#{n}st"
when 2; "#{n}nd"
when 3; "#{n}rd"
else "#{n}th"
end
end
ordinalize 1
=> "1st"
ordinalize 2
=> "2nd"
ordinalize 11
=> "11th"
Using humanize gem, is probably the easiest way. But, yes, it is not built in, however it has only one dependency, so I think its a pretty good choice..
require 'humanize'
2.humanize => "two"