How to skip over null values in Ruby hashes - ruby-on-rails

I have an array in which each array item is a hash with date values, as shown in my example below. In actuality, it is longer and there are about 20 dates per item instead of 3. What I need to do is get the date interval values for each item (that is, how many days between each date value), and their intervals' medians. My code is as follows:
require 'csv'
require 'date'
dateArray = [{:date_one => "May 1", :date_two =>"May 5", :date_three => " "}, {:date_one => "May 10", :date_two =>"May 10", :date_three => "May 20"}, {:date_one => "May 6", :date_two =>"May 11", :date_three => "May 12"}]
public
def median
sorted = self.sort
len = sorted.length
return (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
end
puts dateIntervals = dateArray.map{|h| (DateTime.parse(h[:date_two]) - DateTime.parse(h[:date_one])).to_i}
puts "\nMedian: "
puts dateIntervals.median
Which returns these date interval values and this median:
4
0
5
Median: 4
However, some of these items' values are empty, as in the first item, in its :date_three value. If I try to run the same equations for the :date_three to :date_two values, as follows, it will throw an error because the last :date_three value is empty.
It's okay that I can't get that interval, but I would still would need the next two items date intervals (which would be 10 and 1).
How can I skip over intervals that return errors when I try to run them?

I would recommend adding helper functions that can deal with the types of inputs you're expecting. For instance:
def date_diff(date_one, date_two)
return nil if date_one.nil? || date_two.nil?
(date_one - date_two).to_i
end
def str_to_date(input_string)
DateTime.parse(input_string)
rescue
nil
end
dateArray.map{|h| date_diff(str_to_date(h[:date_three]), str_to_date(h[:date_two])) }
=> [nil, 10, 1]
dateArray.map{|h| date_diff(str_to_date(h[:date_three]), str_to_date(h[:date_two])) }.compact.median
=> 5.5
The bonus here is that you can then add unit tests for the individual components so that you can easily test edge cases (nil dates, empty string dates, etc).

In your map block, you can just add a check to make sure the values aren't blank
dateIntervals = dateArray.map{ |h|
(DateTime.parse(h[:date_two]) - DateTime.parse(h[:date_one])).to_i unless any_blank?(h)
}
def any_blank?(h)
h.each do |k, v|
return true if v == " "
end
end

I would first just filter out the empty values first (I check if the string consists entirely of whitespace or is empty), then compare the remaining values using your existing code. I added a loop which will compare all values in the sequence to the next value.
dateArray = [
{ date_one: "May 1", date_two: "May 5", date_three: " ", date_four: "" },
{ date_one: "May 10", date_two: "May 10", date_three: "May 20" }
]
intervals = dateArray.map do |hash|
filtered = hash.values.reject { |str| str =~ /^\s*$/ }
(0...filtered.size-1).map { |idx| (DateTime.parse(filtered[idx+1]) - DateTime.parse(filtered[idx])).to_i }
end
# => [[4], [0, 10]]

Related

Checking if hash value has a text

I have a hash:
universityname = e.university
topuniversities = CSV.read('lib/assets/topuniversities.csv',{encoding: "UTF-8", headers:true, header_converters: :symbol, converters: :all})
hashed_topuniversities = topuniversities.map {|d| d.to_hash}
hashed_topuniversities.any? {|rank, name| name.split(' ').include?(universityname) }.each do |s|
if s[:universityrank] <= 10
new_score += 10
elsif s[:universityrank] >= 11 && s[:universityrank] <= 25
new_score += 5
elsif s[:universityrank] >= 26 && s[:universityrank] <= 50
new_score += 3
elsif s[:universityrank] >= 51 && s[:universityrank] <= 100
new_score += 2
end
Basically what this is doing is looking at a hash and checking if the hash value contains a university name is an input.
For example the user input can be "Oxford University" and in the hash its stored as "Oxford". The User needs to type in as it stored in the hash to be able to be assigned a score, But I want it that if the user types in "oxford university" then the hash value "Oxford" should be selected and then go through.
Everything else in this works fine but the .include? does not work correctly, I still need to type the exact word.
hashed_topuniversities = topuniversities.map &:to_hash
univ = hashed_topuniversities.detect do |rank, name|
name.downcase.split(' ').include?(universityname.downcase)
end
new_score += case univ[:universityrank]
when -Float::INFINITY..10 then 10
when 11..25 then 5
when 26..50 then 3
when 50..100 then 2
else 0
end
Besides some code improvements in terms of being more idiomatic ruby, the main change is downcase called on both university name and user input. Now they are compared case insensitive.
I don't think your approach will work (in real-life, anyway). "University of Oxford" is an easy one--just look for the presence of the word, "Oxford". What about "University of Kansas"? Would you merely try to match "Kansas"? What about "Kansas State University"?
Also, some universities are are customarily referred to by well-know acronyms or shortened names, such as "LSE", "UCLA", "USC", "SUNY", "LSU", "RPI", "Penn State", "Georgia Tech", "Berkeley" and "Cal Tech". You also need to think about punctuation and "little words" (e.g., "at", "the", "of") in university names (e.g., "University of California, Los Angeles").
For any serious application, I think you need to construct a list of all commonly-used names for each university and then require an exact match between those names and the given university name (after punctuation and little words have been removed). You can do that by modifying the hash hashed_top_universities, perhaps like this:
hashed_top_universities
#=> { "University of California at Berkeley" =>
# { rank: 1, names: ["university california", "berkeley", "cal"] },
# "University of California at Los Angeles" =>
# { rank: 2, names: ["ucla"] },
# "University of Oxford" =>
# { rank: 3, names: ["oxford", "oxford university"] }
# }
Names of some universities contain non-ASCII characters, which is a further complication (that I will not address).
Here's how you might code it.
Given a university name, the first step is to construct a hash (reverse_hash) that maps university names to ranks. The names consist of the elements of the value of the key :names in the inner hashes in hashed_top_universities, together with the complete university names that comprise the keys in that hash, after they have been downcased and punctuation and "little words" have been removed.
PUNCTUATION = ",."
EXCLUSIONS = %w| of for the at u |
SCORE = { 1=>10, 3=>7, 25=>5, 50=>3, 100=>2, Float::INFINITY=>0 }
reverse_hash = hashed_top_universities.each_with_object({}) { |(k,v),h|
(v[:names] + [simplify(k)]).each { |name| h[name] = v[:rank] } }
#=> {"university california"=>1, "berkeley"=>1, "cal"=>1,
# "university california berkeley"=>1,
# "ucla"=>2, "university california los angeles"=>2,
# "oxford"=>3, "oxford university"=>3, "university oxford"=>3}
def simplify(str)
str.downcase.delete(PUNCTUATION).
gsub(/\b#{Regexp.union(EXCLUSIONS)}\b/,'').
squeeze(' ')
end
def score(name, reverse_hash)
rank = reverse_hash[simplify(name)]
SCORE.find { |k,_| rank <= k }.last
end
Let's try it.
score("University of California at Berkeley", reverse_hash)
#=> 10
score("Cal", reverse_hash)
#=> 10
score("UCLA", reverse_hash)
#=> 7
score("Oxford", reverse_hash)
#=> 7

How can you sort an array in Ruby starting at a specific letter, say letter f?

I have a text array.
text_array = ["bob", "alice", "dave", "carol", "frank", "eve", "jordan", "isaac", "harry", "george"]
text_array = text_array.sort would give us a sorted array.
However, I want a sorted array with f as the first letter for our order, and e as the last.
So the end result should be...
text_array = ["frank", "george", "harry", "isaac", "jordan", "alice", "bob", "carol", "dave", "eve"]
What would be the best way to accomplish this?
Try this:
result = (text_array.select{ |v| v =~ /^[f-z]/ }.sort + text_array.select{ |v| v =~ /^[a-e]/ }.sort).flatten
It's not the prettiest but it will get the job done.
Edit per comment. Making a more general piece of code:
before = []
after = []
text_array.sort.each do |t|
if t > term
after << t
else
before << t
end
end
return (after + before).flatten
This code assumes that term is whatever you want to divide the array. And if an array value equals term, it will be at the end.
You can do that using a hash:
alpha = ('a'..'z').to_a
#=> ["a", "b", "c",..."x", "y", "z"]
reordered = alpha.rotate(5)
#=> ["f", "g",..."z", "a",...,"e"]
h = reordered.zip(alpha).to_h
# => {"f"=>"a", "g"=>"b",..., "z"=>"u", "a"=>"v",..., e"=>"z"}
text_array.sort_by { |w| w.gsub(/./,h) }
#=> ["frank", "george", "harry", "isaac", "jordan",
# "alice", "bob", "carol", "dave", "eve"]
A variant of this is:
a_to_z = alpha.join
#=> "abcdefghijklmnopqrstuvwxyz"
f_to_e = reordered.join
#=> "fghijklmnopqrstuvwxyzabcde"
text_array.sort_by { |w| w.tr(f_to_e, a_to_z) }
#=> ["frank", "george", "harry", "isaac", "jordan",
# "alice", "bob", "carol", "dave", "eve"]
I think the easiest would be to rotate the sorted array:
text_array.rotate(offset) if offset = text_array.find_index { |e| e.size > 0 and e[0] == 'f' }
Combining Ryan K's answer and my previous answer, this is a one-liner you can use without any regex:
text_array = text_array.sort!.select {|x| x.first >= "f"} + text_array.select {|x| x.first < "f"}
If I got your question right, it looks like you want to create sorted list with biased predefined patterns.
ie. let's say you want to define specific pattern of text which can completely change the sorting sequence for the array element.
Here is my proposal, you can get better code out of this, but my tired brain got it for now -
an_array = ["bob", "alice", "dave", "carol", "frank", "eve", "jordan", "isaac", "harry", "george"]
# Define your patterns with scores so that the sorting result can vary accordingly
# It's full fledged Regex so you can put any kind of regex you want.
patterns = {
/^f/ => 100,
/^e/ => -100,
/^g/ => 60,
/^j/ => 40
}
# Sort the array with our preferred sequence
sorted_array = an_array.sort do |left, right|
# Find score for the left string
left_score = patterns.find{ |p, s| left.match(p) }
left_score = left_score ? left_score.last : 0
# Find the score for the right string
right_score = patterns.find{ |p, s| right.match(p) }
right_score = right_score ? right_score.last : 0
# Create the comparision score to prepare the right order
# 1 means replace with right and -1 means replace with left
# and 0 means remain unchanged
score = if right_score > left_score
1
elsif left_score > right_score
-1
else
0
end
# For debugging purpose, I added few verbose data
puts "L#{left_score}, R:#{right_score}: #{left}, #{right} => #{score}"
score
end
# Original array
puts an_array.join(', ')
# Biased array
puts sorted_array.join(', ')

What is the most effective way to obtain an array of Date objects between two dates in Ruby?

So I have two dates
date_start = Date("2014", "11", "1")
date_stop = Date("2014", "12", "25")
if I want an array of Date objects between these two dates, what would be the most efficient methods ?
For an interval of 1.day between dates:
(date1..date2).to_a
For other intervals, you'll have to populate an array yourself.
interval = 2.days
[date1].tap do |arr|
until (arr.last >= date2)
new_date = arr.last + interval
# case 1: if you want the array to end on date2:
arr << [new_date, date2].min
# case 2: if you want the array to be equally spaced:
arr << new_date
# case 3: if you want the array to be equally spaced,
# but values to be within date1 and date2:
if (new_date <= date2)
arr << new_date
else
break
end
end
end
Normally you can use the step method on a range to specify the interval; but not in this case. The following, for example, doesn't work as you expect it to:
(date1..date2).step(2.days).to_a # outputs: [date1]
As #Humza has said, you should use the Ranges to solve your problem. You'll be able to:
include or exclude the last value by using .. or ...
define the step you want with .step(YOUR_STEP)
There is an example :
require 'date'
date_start = Date.new(2014, 11, 1)
date_stop = Date.new(2014, 11, 6)
including_last_date = (date_start..date_stop).step(5).to_a
excluding_last_date = (date_start...date_stop).step(5).to_a
puts "INCLUDING : #{including_last_date.map(&:to_s)}" # INCLUDING : ["2014-11-01", "2014-11-06"]
puts "EXCLUDING : #{excluding_last_date.map(&:to_s)}" # EXCLUDING : ["2014-11-01"]
I hope this helps!

How to parse values in multidimensional array and select one over the other based on condition?

I have an array that behaves like a multidimensional array through spaces, like:
"roles"=>["1 editor 0", "1 editor 1", "2 editor 0", "2 editor 1", "14 editor 0", "15 editor 0"], "commit"=>"Give Access", "id"=>"3"}
Each array value represents [category_id, user.title, checked_boolean], and comes from
form
<%= hidden_field_tag "roles[]", [c.id, "editor", 0] %>
<%= check_box_tag "roles[]", [c.id, "editor", 1 ], !!checked %>
which I process it using splits
params[:roles].each do |role|
cat_id = role[0].split(" ")[0]
title = role.split(" ")[1]
checked_boolean = role.split(" ")[2]
end
Given the array at the top, you can see that the "Category 1" & "Category 2" is checked, while "Cat 14" and "Cat 15" are not.
I would like to compare the values of the given array, and if both 1 & 0 exists for a given category_id, I would like to get rid of the value with "checked_boolean = 0". This way, if the boolean is a 1, I can check to see if the Role already exists, and if not, create it. And if it is 0, I can check to see if Role exists, and if it does, delete it.
How would I be able to do this? I thought of doing something like params[:roles].uniq but didn't know how to process the uniq only on the first split.
Or is there a better way of posting the "unchecks" in Rails? I've found solutions for processing the uncheck action for simple checkboxes that passes in either true/false, but my case is different because it needs to pass in true/false in addition to the User.Title
Let's params[:roles] is:
["1 editor 0", "1 editor 1", "2 editor 0", "2 editor 1", "14 editor 0", "15 editor 0"]
The example of the conversion and filtering is below:
roles = params[:roles].map {| role | role.split " " }
filtered = roles.select do| role |
next true if role[ 2 ].to_i == 1
count = roles.reduce( 0 ) {| count, r | r[ 0 ] == role[ 0 ] && count + 1 || count}
count == 1
end
# => [["1", "editor", "1"], ["2", "editor", "1"], ["14", "editor", "0"], ["15", "editor", "0"]]
filtered.map {| role | role.join( ' ' ) }
Since the select method returns a new filtered role array, so result array you can see above. But of course you can still use and source params[:roles], and intermediate (after map method worked) versions of role array.
Finally you can adduce the result array into the text form:
filtered.map {| role | role.join( ' ' ) }
=> ["1 editor 1", "2 editor 1", "14 editor 0", "15 editor 0"]
majioa's solution is certainly more terse and a better use of the language's features, but here is my take on it with a more language agnostic approach. I have only just started learning Ruby so I used this as an opportunity to learn, but it does solve your problem.
my_array = ["1 editor 0", "1 editor 0", "1 editor 1", "2 editor 0",
"2 editor 1", "14 editor 0", "15 editor 0"]
puts "My array before:"
puts my_array.inspect
# As we're nesting a loop inside another for each loop
# we can't delete from the same array without confusing the
# iterator of the outside loop. Instead we'll delete at the end.
role_to_del = Array.new
my_array.each do |role|
cat_id, checked_boolean = role.split(" ")[0], role.split(" ")[2]
if checked_boolean == "1"
# Search through the array and mark the roles for deletion if
# the category id's match and the found role's checked status
# doesn't equal 1.
my_array.each do |s_role|
s_cat_id = s_role.split(" ")[0]
if s_cat_id != cat_id
next
else
s_checked_boolean = s_role.split(" ")[2]
role_to_del.push s_role if s_checked_boolean != "1"
end
end
end
end
# Delete all redundant roles
role_to_del.each { |role| my_array.delete role }
puts "My array after:"
puts my_array.inspect
Output:
My array before:
["1 editor 0", "1 editor 0", "1 editor 1", "2 editor 0", "2 editor 1", "14 editor 0",
"15 editor 0"]
My array after:
["1 editor 1", "2 editor 1", "14 editor 0", "15 editor 0"]

There has got to be a cleaner way to do this

I have this code here and it works but there has to be a better way.....i need two arrays that look like this
[
{
"Vector Arena - Auckland Central, New Zealand" => {
"2010-10-10" => [
"Enter Sandman",
"Unforgiven",
"And justice for all"
]
}
},
{
"Brisbane Entertainment Centre - Brisbane Qld, Austr..." => {
"2010-10-11" => [
"Enter Sandman"
]
}
}
]
one for the past and one for the upcoming...the problem i have is i am repeating myself and though it works i want to clean it up ...here is my data
..
Try this:
h = Hash.new {|h1, k1| h1[k1] = Hash.new{|h2, k2| h2[k2] = []}}
result, today = [ h, h.dup], Date.today
Request.find_all_by_artist("Metallica",
:select => "DISTINCT venue, showdate, LOWER(song) AS song"
).each do |req|
idx = req.showdate < today ? 0 : 1
result[idx][req.venue][req.showdate] << req.song.titlecase
end
Note 1
In the first line I am initializing an hash of hashes. The outer hash creates the inner hash when a non existent key is accessed. An excerpt from Ruby Hash documentation:
If this hash is subsequently accessed by a key that doesn‘t correspond to a hash
entry, the block will be called with the hash object and the key, and should
return the default value. It is the block‘s responsibility to store the value in
the hash if required.
The inner hash creates and empty array when the non existent date is accessed.
E.g: Construct an hash containing of content as values and date as keys:
Without a default block:
h = {}
list.each do |data|
h[data.date] = [] unless h[data.date]
h[data.date] << data.content
end
With a default block
h = Hash.new{|h, k| h[k] = []}
list.each do |data|
h[data.date] << data.content
end
Second line simply creates an array with two items to hold the past and future data. Since both past and the present stores the data as Hash of Hash of Array, I simply duplicate the value.
Second line can also be written as
result = [ h, h.dup]
today = Date.today

Resources