Given two large arrays of ranges...
A = [0..23, 30..53, 60..83, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]
When I do a logical conjuction...
C = A.mask(B)
Then I expect
describe "Array#mask" do
it{expect(C = A.mask(B)).to eq([0..13, 30..33, 45..53, 65..73, 90..93])}
end
It feels like it should be...
C = A & B
=> []
but that's empty because none of the ranges are identical.
Here's a pictorial example.
.
I've included Infinity in the range because solutions to this problem typically involve converting the Range to an Array or Set.
My current solution
This is my current solution with passing tests for speed and accuracy. I was looking for comments and/or suggested improvements. The second test uses the excellent IceCube gem to generate an array of date ranges. There's an implicit assumption in my mask method that date range occurrences within each schedule do not overlap.
require 'pry'
require 'rspec'
require 'benchmark'
require 'chronic'
require 'ice_cube'
require 'active_support'
require 'active_support/core_ext/numeric'
require 'active_support/core_ext/date/calculations'
A = [0..23, 30..53, 60..83, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]
class Array
def mask(other)
a_down = self.map{|r| [:a, r.max]}
a_up = self.map{|r| [:a, r.min]}
b_down = other.map{|r| [:b, r.max]}
b_up = other.map{|r| [:b, r.min]}
up = a_up + b_up
down = a_down + b_down
a, b, start, result = false, false, nil, []
ticks = (up + down).sort_by{|i| i[1]}
ticks.each do |tick|
tick[0] == :a ? a = !a : b = !b
result << (start..tick[1]) if !start.nil?
start = a & b ? tick[1] : nil
end
return result
end
end
describe "Array#mask" do
context "simple integer array" do
it{expect(C = A.mask(B)).to eq([0..13, 30..33, 45..53, 65..73, 90..93])}
end
context "larger date ranges from IceCube schedule" do
it "should take less than 0.1 seconds" do
year = Time.now..(Time.now + 52.weeks)
non_premium_schedule = IceCube::Schedule.new(Time.at(0)) do |s|
s.duration = 12.hours
s.add_recurrence_rule IceCube::Rule.weekly.day(:monday, :tuesday, :wednesday, :thursday, :friday).hour_of_day(7).minute_of_hour(0)
end
rota_schedule = IceCube::Schedule.new(Time.at(0)) do |s|
s.duration = 7.hours
s.add_recurrence_rule IceCube::Rule.weekly(2).day(:tuesday).hour_of_day(15).minute_of_hour(30)
end
np = non_premium_schedule.occurrences_between(year.min, year.max).map{|d| d..d+non_premium_schedule.duration}
rt = rota_schedule.occurrences_between(year.min, year.max).map{|d| d..d+rota_schedule.duration}
expect(Benchmark.realtime{np.mask(rt)}).to be < 0.1
end
end
end
It feels odd that you can't do this with Ruby's existing core methods? Am I missing something? I find myself calculating range intersections on a fairly regular basis.
It also occurred to me that you could use the same method to find an intersection between two single ranges by passing single item arrays. e.g.
[(54..99)].mask[(65..120)]
I realise I've kind of answered my own question but thought I would leave it here as a reference for others.
I'm not sure I really understand your question; I'm a little confused by your expect statement, and I don't know why your arrays aren't the same size. That said, if you want to calculate the intersection of two ranges, I like this monkey-patch (from Ruby: intersection between two ranges):
class Range
def intersection(other)
return nil if (self.max < other.begin or other.max < self.begin)
[self.begin, other.begin].max..[self.max, other.max].min
end
alias_method :&, :intersection
end
and then you can do:
A = [0..23, 30..53, 60..83, 0..0, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]
A.zip(B).map { |x, y| x & y }
# => [0..13, 30..33, nil, nil, 90..93]
which seems a reasonable result...
EDIT
If you monkeypatch Range as posted above, and then do:
# your initial data
A = [0..23, 30..53, 60..83, 90..113]
B = [-Float::INFINITY..13, 25..33, 45..53, 65..73, 85..93]
A.product(B).map {|x, y| x & y }.compact
# => [0..13, 30..33, 45..53, 65..73, 90..93]
You get the results you specify. No idea how it compares performance-wise, and I'm not sure about the sort order...
Related
I have built a version of mastermind that checks a user's input and provides feedback based on how close the user's guess was to the winning sequence. If you're not familiar with the game, you get feedback indicating how many of your characters were guessed correctly at the same index and how many characters guessed are in the sequence, but at the wrong index. If there are duplicates in the guess, then you would not count the extra values unless they correspond to the same number of duplicates in the secret code.
Example: If the sequence is ["G","G","G","Y"] and the user guesses ["G", "Y","G","G"] then you'd want to return 2 for items at the same index and 2 for items at different indexes that are included in the secret sequence.
Another example: If the sequence is ["X","R","Y","T"] and the user guesses ["T","T","Y","Y"] then you'd return 1 for items at the same index 1 for the character guessed that is in the sequence but at the wrong index.
Anyway, to me this is not a simple problem to solve. Here's the code I used to get it to work, but it's not elegant. There must be a better way. I was hoping someone can tell me what I'm missing here?? New to Ruby...
def index_checker(input_array, sequence_array)
count = 0
leftover_input = []
leftover_sequence = []
input.each_with_index do |char, idx|
if char == sequence[idx]
count += 1
else
leftover_input << char
leftover_sequence << sequence[idx]
end
end
diff_index_checker(leftover_input, leftover_sequence, count)
end
def diff_index_checker(input, sequence, count)
count2 = 0
already_counted = []
input.each do |char|
if sequence.include?(char) && !already_counted.include?(char)
count2 += 1
already_counted << char
end
end
[count, count2]
end
Here's a clean Ruby solution, written in idiomatic Ruby object-oriented style:
class Mastermind
def initialize(input_array, sequence_array)
#input_array = input_array
#sequence_array = sequence_array
end
def matches
[index_matches, other_matches]
end
def results
[index_matches.size, other_matches.size]
end
private
attr_reader :input_array, :sequence_array
def index_matches
input_array.select.with_index { |e, i| e == sequence_array[i] }
end
def other_matches
non_exact_input & non_exact_sequence
end
def non_exact_input
array_difference(input_array, index_matches)
end
def non_exact_sequence
array_difference(sequence_array, index_matches)
end
# This method is based on https://stackoverflow.com/a/3852809/5961578
def array_difference(array_1, array_2)
counts = array_2.inject(Hash.new(0)) { |h, v| h[v] += 1; h }
array_1.reject { |e| counts[e] -= 1 unless counts[e].zero? }
end
end
You would use this class as follows:
>> input_array = ["G","G","G","Y"]
>> sequence_array = ["G", "Y","G","G"]
>> guess = Mastermind.new(input_array, sequence_array)
>> guess.results
#> [2, 2]
>> guess.matches
#> [["G", "G"], ["G", "Y"]]
Here's how it works. First everything goes into a class called Mastermind. We create a constructor for the class (which in Ruby is a method called initialize) and we have it accept two arguments: input array (the user guess), and sequence array (the answer).
We set each of these arguments to an instance variable, which is indicated by its beginning with #. Then we use attr_reader to create getter methods for #input_array and #sequence_array, which allows us to get the values by calling input_array and sequence_array from any instance method within the class.
We then define two public methods: matches (which returns an array of exact matches and an array of other matches (the ones that match but at the wrong index), and results (which returns a count of each of these two arrays).
Now, within the private portion of our class, we can define the guts of the logic. Each method has a specific job, and each is named to (hopefully) help a reader understand what it is doing.
index_matches returns a subset of the input_array whose elements match the sequence_array exactly.
other_matches returns a subset of the input_array whose elements do not match the sequence_array exactly, but do match at the wrong index.
other_matches relies on non_exact_input and non_exact_sequence, each of which is computed using the array_difference method, which I copied from another SO answer. (There is no convenient Ruby method that allows us to subtract one array from another without deleting duplicates).
Code
def matches(hidden, guess)
indices_wo_match = hidden.each_index.reject { |i| hidden[i] == guess[i] }
hidden_counts = counting_hash(hidden.values_at *indices_wo_match)
guess_counts = counting_hash(guess.values_at *indices_wo_match)
[hidden.size - indices_wo_match.size, guess_counts.reduce(0) { |tot, (k, cnt)|
tot + [hidden_counts[k], cnt].min }]
end
def counting_hash(arr)
arr.each_with_object(Hash.new(0)) { |s, h| h[s] += 1 }
end
Examples
matches ["G","G","G","Y"], ["G", "Y","G","G"]
#=> [2, 2]
matches ["X","R","Y","T"] , ["T","T","Y","Y"]
#=> [1, 1]
Explanation
The steps are as follows.
hidden = ["G","G","G","Y"]
guess = ["G", "Y","G","G"]
Save the indices i for which hidden[i] != guess[i].
indices_wo_match = hidden.each_index.reject { |i| hidden[i] == guess[i] }
#=> [1, 3]
Note that the number of indices for which the values are equal is as follows.
hidden.size - indices_wo_match.size
#=> 2
Now compute the numbers of remaining elements of guess that pair with one of the remaining values of hidden by having the same value. Begin by counting the numbers of instances of each unique element of hidden and then do the same for guess.
hidden_counts = counting_hash(hidden.values_at *indices_wo_match)
#=> {"G"=>1, "Y"=>1}
guess_counts = counting_hash(guess.values_at *indices_wo_match)
#=> {"Y"=>1, "G"=>1}
To understand how counting_hash works, see Hash::new, especially the explanation of the effect of providing a default value as an argument of new. In brief, if a hash is defined h = Hash.new(3), then if h does not have a key k, h[k] returns the default value, here 3 (the hash is not changed).
Now compute the numbers of matches of elements of guess that were not equal to the value of hidden at the same index and which pair with an element of hidden that have the same value.
val_matches = guess_counts.reduce(0) do |tot, (k, cnt)|
tot + [hidden_counts[k], cnt].min
end
#=> 2
Lastly, return the values of interest.
[hidden.size - indices_wo_match.size, val_matches]
#=> [2, 2]
In the code presented above I have substituted out the variable val_matches.
With Ruby 2.4+ one can use Enumerable#sum to replace
guess_counts.reduce(0) { |tot, (k, cnt)| tot + [hidden_counts[k], cnt].min }
with
guess_counts.sum { |k, cnt| [hidden_counts[k], cnt].min }
def judge(secret, guess)
full = secret.zip(guess).count { |s, g| s == g }
semi = secret.uniq.sum { |s| [secret.count(s), guess.count(s)].min } - full
[full, semi]
end
Demo:
> judge(["G","G","G","Y"], ["G","Y","G","G"])
=> [2, 2]
> judge(["X","R","Y","T"], ["T","T","Y","Y"])
=> [1, 1]
A shorter alternative, though I find it less clear:
full = secret.zip(guess).count(&:uniq!)
I prefer my other answer for its simplicity, but this one would be faster if someone wanted to use this for arrays larger than Mastermind's.
def judge(secret, guess)
full = secret.zip(guess).count { |s, g| s == g }
pool = secret.group_by(&:itself)
[full, guess.count { |g| pool[g]&.pop } - full]
end
Demo:
> judge(["G","G","G","Y"], ["G","Y","G","G"])
=> [2, 2]
> judge(["X","R","Y","T"], ["T","T","Y","Y"])
=> [1, 1]
I have a number of ranges that I want merge together if they overlap. The way I’m currently doing this is by using Sets.
This is working. However, when I attempt the same code with a larger ranges as follows, I get a `stack level too deep (SystemStackError).
require 'set'
ranges = [Range.new(73, 856), Range.new(82, 1145), Range.new(116, 2914), Range.new(3203, 3241)]
set = Set.new
ranges.each { |r| set << r.to_set }
set.flatten!
sets_subsets = set.divide { |i, j| (i - j).abs == 1 } # this line causes the error
puts sets_subsets
The line that is failing is taken directly from the Ruby Set Documentation.
I would appreciate it if anyone could suggest a fix or an alternative that works for the above example
EDIT
I have put the full code I’m using here:
Basically it is used to add html tags to an amino acid sequence according to some features.
require 'set'
def calculate_formatting_classes(hsps, signalp)
merged_hsps = merge_ranges(hsps)
sp = format_signalp(merged_hsps, signalp)
hsp_class = (merged_hsps - sp[1]) - sp[0]
rank_format_positions(sp, hsp_class)
end
def merge_ranges(ranges)
set = Set.new
ranges.each { |r| set << r.to_set }
set.flatten
end
def format_signalp(merged_hsps, sp)
sp_class = sp - merged_hsps
sp_hsp_class = sp & merged_hsps # overlap regions between sp & merged_hsp
[sp_class, sp_hsp_class]
end
def rank_format_positions(sp, hsp_class)
results = []
results += sets_to_hash(sp[0], 'sp')
results += sets_to_hash(sp[1], 'sphsp')
results += sets_to_hash(hsp_class, 'hsp')
results.sort_by { |s| s[:pos] }
end
def sets_to_hash(set = nil, cl)
return nil if set.nil?
hashes = []
merged_set = set.divide { |i, j| (i - j).abs == 1 }
merged_set.each do |s|
hashes << { pos: s.min.to_i - 1, insert: "<span class=#{cl}>" }
hashes << { pos: s.max.to_i - 0.1, insert: '</span>' } # for ordering
end
hashes
end
working_hsp = [Range.new(7, 136), Range.new(143, 178)]
not_working_hsp = [Range.new(73, 856), Range.new(82, 1145),
Range.new(116, 2914), Range.new(3203, 3241)]
sp = Range.new(1, 20).to_set
# working
results = calculate_formatting_classes(working_hsp, sp)
# Not Working
# results = calculate_formatting_classes(not_working_hsp, sp)
puts results
Here is one way to do this:
ranges = [Range.new(73, 856), Range.new(82, 1145),
Range.new(116, 2914), Range.new(3203, 3241)]
ranges.size.times do
ranges = ranges.sort_by(&:begin)
t = ranges.each_cons(2).to_a
t.each do |r1, r2|
if (r2.cover? r1.begin) || (r2.cover? r1.end) ||
(r1.cover? r2.begin) || (r1.cover? r2.end)
ranges << Range.new([r1.begin, r2.begin].min, [r1.end, r2.end].max)
ranges.delete(r1)
ranges.delete(r2)
t.delete [r1,r2]
end
end
end
p ranges
#=> [73..2914, 3203..3241]
The other answers aren't bad, but I prefer a simple recursive approach:
def merge_ranges(*ranges)
range, *rest = ranges
return if range.nil?
# Find the index of the first range in `rest` that overlaps this one
other_idx = rest.find_index do |other|
range.cover?(other.begin) || other.cover?(range.begin)
end
if other_idx
# An overlapping range was found; remove it from `rest` and merge
# it with this one
other = rest.slice!(other_idx)
merged = ([range.begin, other.begin].min)..([range.end, other.end].max)
# Try again with the merged range and the remaining `rest`
merge_ranges(merged, *rest)
else
# No overlapping range was found; move on
[ range, *merge_ranges(*rest) ]
end
end
Note: This code assumes each range is ascending (e.g. 10..5 will break it).
Usage:
ranges = [ 73..856, 82..1145, 116..2914, 3203..3241 ]
p merge_ranges(*ranges)
# => [73..2914, 3203..3241]
ranges = [ 0..10, 5..20, 30..50, 45..80, 50..90, 100..101, 101..200 ]
p merge_ranges(*ranges)
# => [0..20, 30..90, 100..200]
I believe your resulting set has too many items (2881) to be used with divide, which if I understood correctly, would require 2881^2881 iterations, which is such a big number (8,7927981983090337174360463368808e+9966) that running it would take nearly forever even if you didn't get stack level too deep error.
Without using sets, you can use this code to merge the ranges:
module RangeMerger
def merge(range_b)
if cover?(range_b.first) && cover?(range_b.last)
self
elsif cover?(range_b.first)
self.class.new(first, range_b.last)
elsif cover?(range_b.last)
self.class.new(range_b.first, last)
else
nil # Unmergable
end
end
end
module ArrayRangePusher
def <<(item)
if item.kind_of?(Range)
item.extend RangeMerger
each_with_index do |own_item, idx|
own_item.extend RangeMerger
if new_range = own_item.merge(item)
self[idx] = new_range
return self
end
end
end
super
end
end
ranges = [Range.new(73, 856), Range.new(82, 1145), Range.new(116, 2914), Range.new(3203, 3241)]
new_ranges = Array.new
new_ranges.extend ArrayRangePusher
ranges.each do |range|
new_ranges << range
end
puts ranges.inspect
puts new_ranges.inspect
This will output:
[73..856, 82..1145, 116..2914, 3203..3241]
[73..2914, 3203..3241]
which I believe is the intended output for your original problem. It's a bit ugly, but I'm a bit rusty at the moment.
Edit: I don't think this has anything to do with your original problem before the edits which was about merging ranges.
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.
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.
So I'm new to programming, and I'm working on Chris Pine's Learn to Program, which teaches Ruby. I'm on chapter 10 trying to make my own method for an array. I was at a total loss and tried modelling mine off his suggested answer. After fiddling around, I can't get an output. I run the program and it simply ends. I even tried using his code and it's giving me the same problem.
Here's what I have so far.
unsorted_array = ['gamma', 'delta', 'beta', 'alpha', 'zeta']
sorted_array = []
def sort some_array
recursive_sort(some_array, [])
end
def recursive_sort(unsorted_array, sorted_array)
if unsorted_array.length <= 0
return sorted_array
end
still_unsorted =[]
smallest = unsorted_array.pop
sorted_array = []
unsorted_array.each do |tested_obj|
if '#{tested_obj}' > smallest
sorted_array.push(smallest)
else
still_unsorted.push(smallest)
smallest = unsorted_array.pop
end
end
recursive_sort(still_unsorted, sorted_array)
end
puts sort(recursive_sort(unsorted_array, sorted_array))
Any advice would be appreciated.
Here are a few observations about your code:
since test_obj is a string, '#{tested_obj}' is the same as #{tested_obj}, which is the same as tested_obj.
declaring sorted_array = [] has no effect. Being a local variable, it is not within the scope of teh method recursive_sort. That method receives an array that it calls sorted_array, so you would not want it initialized anyway.
you don't need to create the new array, still_unsorted; simply transfer elements from unsorted_array to sorted_array.
Below I've fixed and tightened up your code.
def recursive_sort(unsorted_array, sorted_array = [])
return sorted_array unless unsorted_array.length > 0
smallest = unsorted_array.min
unsorted_array.each {|e| sorted_array << e if e == smallest}
unsorted_array.delete(smallest)
recursive_sort(unsorted_array, sorted_array)
end
unsorted_array = ['gamma', 'alpha', 'delta', 'beta', 'gamma', 'alpha', 'zeta']
p recursive_sort unsorted_array
# => ["alpha", "alpha", "beta", "delta", "gamma", "gamma", "zeta"]
Here's what's happening:
by giving the second argument of recursive_sort (sorted_value) a default value of [] (an empty array), there is no need for the method sort you had previously.
sorted_array is returned if sorting is finished (same as return sorted_array if unsorted_array.length == 0).
use Enumerable#min to find the smallest value of the unsorted items (smallest).
add each instance of smallest in unsorted_array to sorted_array.
delete all instances of smallest in unsorted_array.
call the same method again, to remove the next smallest unsorted item, etc.
Note
unsorted_array.each {|e| sorted_array << e if e == smallest}
could be expressed in many different ways. Here's one:
sorted_array += [smallest]*(unsorted_array.count {|e| e == smallest})
To see how this works, suppose smallest = 'alpha'. Then
unsorted_array.count {|e| e == 'alpha'} # => 2
so the above expression is:
sorted_array += ['alpha']*2
which is
sorted_array += ['alpha', 'alpha']
which adds two "alpha"'s to sorted_array.