How to refer the index of an element in Array#delete_if - ruby

I want to build a custom method Array#drop_every(n) (I know it's monkey patching, I am doing this for a homework), which returns a new array omitting every nth element:
[4, 8, 15, 16, 23, 42].drop_every(2) # [4, 15, 23]
I want to implement it with Array#delete_if, but by referring to the index and not to the element itself, (similar to each_index) something like this:
def drop_every(step)
self.delete_if { |index| index % step == 0 }
end
How do I do this? I don't insist on using delete_if, I also looked at drop_while and reject, other suggestions are welcome.

You can use with_index method that returns enumerator, filter your collection and then get rid of the indexes.
class Array
def drop_every(step)
self.each.with_index.select { |_, index| index % step == 0 }.map(&:first)
end
end
[4, 8, 15, 16, 23, 42].drop_every(2) # => [4, 15, 23]

def drop_every(step)
reject.with_index { |x,i| (i+1) % step == 0 }
end
[4, 8, 15, 16, 23, 42].reject.with_index{|x,i| (i+1) % 2 == 0}
# => [4, 15, 23]
[4, 8, 15, 16, 23, 42].reject.with_index{|x,i| (i+1) % 3 == 0}
# => [4, 8, 16, 23]

You could use the values_at method to selectively filter out indices which you want.
class Array
def drop_every(step)
self.values_at(*(0...self.size).find_all{ |x| (x+1) % step != 0 })
end
end
The answer was accepted while I was typing it. I will post it anyways.

def drop_every step
delete_if.with_index(1){|_, i| i.%(step).zero?}
end

class Array
def drop_every(step)
self.each_slice(step).flat_map{|slice| slice[0..-2]}
end
end
p [4, 8, 15, 16, 23, 42].drop_every(2) #=> [4, 15, 23]

I'd extend the Enumerable mixin instead:
module Enumerable
def drop_every(step)
return to_enum(:drop_every, step) unless block_given?
each.with_index(1) do |o, i|
yield o unless i % step == 0
end
end
end
(1..10).drop_every(3) { |a| p a }
# outputs below
1
2
4
5
7
8
10

Related

Remove groups of consecutive numbers from array in Ruby

I have an array:
[1, 2, 3, 6, 8, 9, 10, 23, 34, 35, 36, 45, 50, 51, ...]
I'm trying to remove each group of consecutive numbers so I end up with:
[6, 23, 45, ...]
I am looking for anomalies in serial ids. Does anyone have suggestions?
My initial attempt only checks for the id before each element:
non_consecutive_ids = []
ids.each_with_index do |x, i|
unless x == ids[i-1] + 1
non_consecutive_ids << x
end
end
The thing I think I was missing was to also check to see if the next element in the array is 1 more than the current.
Other option:
array.chunk_while { |i, j| i + 1 == j }.select { |e| e.size == 1 }.flatten
#=> [6, 23, 45]
The good of Enumerable#chunk_while is that it takes two params. The core doc has just an example of a one-by-one increasing subsequence.
You can use select and check the surrounding values:
array.select.with_index{ |x, index| (array[index-1] != x-1) && (array[index+1] != x+1)}

is there a Range in Ruby like x-y not x .. y

I'm trying to solve Range Extraction
each time i click Attempt i see that
Test Results:
✘ Expected: "-6,-3-1,3-5,7-11,14,15,17-20", instead got: [-6, -3..1, 3..5, 7..11, 14, 15, 17..20]
so what is the different between -3-1 and -3..1 ? is that a bug ?
this is the first day i write Ruby so i can not judge
this is my code
def solution(list)
result = []
arr = []
list.each.with_index{
|x,index|
arr.push(x)
if index == list.length-1
result.push(arr)
break
end
if list[index + 1] - x != 1
result.push(arr)
arr = []
end
}
final = []
result.each{
|x|
if x.length >= 3
final.push(Range.new(x[0],x[-1]))
else
final.concat(x)
end
}
final
end
puts solution([-6, -3, -2, -1, 0, 1, 3, 4, 5, 7, 8, 9, 10, 11, 14, 15, 17, 18, 19, 20]).inspect
# returns "-6,-3-1,3-5,7-11,14,15,17-20"
Thanks to slice_when, you can write a shorter and more Ruby-ish solution:
def solution(list)
list.slice_when { |x, y| y > x + 1 }.flat_map do |neighbors|
if neighbors.size > 2
"#{neighbors.first}-#{neighbors.last}"
else
neighbors
end
end.join(',')
end
"-6,-3-1,3-5,7-11,14,15,17-20" is a string, [-6, -3..1, 3..5, 7..11, 14, 15, 17..20] is an array of ranges and integers.
You can replace the final line of your solution method to coerce the array to the required format:
final.map do |x|
if x.is_a? Range
[x.min, x.max].join("-")
else
x.to_s
end
end.join(",")
Here are two other ways.
array = [-6, -3, -2, -1, 0, 3, 5, 7, 8, 9, 14, 15, 17]
#1 Use Enumerable#chunk_while
Enumerable#chunk_while (new in Ruby v2.3) is the flip side of Enumerable#slice_when (new in Ruby v2.2), which #Eric used in his answer.
array.chunk_while { |x,y| y == x+1 }.map { |a|
a.size == 1 ? a.first.to_s : "#{ a.first }-#{ a.last }" }.join(",")
#=> "-6,-3-0,3,5,7-9,14-15,17"
Note that
array.chunk_while { |a,b| b == a+1 }.to_a
#=> [[-6], [-3, -2, -1, 0], [3], [5], [7, 8, 9], [14, 15], [17]]
#2 Step through the array
first, *rest = array
rest.each_with_object([[first]]) { |n, arr|
(n == arr.last.last+1) ? (arr.last << n) : (arr << [n]) }.
map { |a| (a.size == 1)? a.first.to_s : "#{ a.first }-#{ a.last }" }.join(",")
#=> "-6,-3-0,3,5,7-9,14-15,17"

error in modulus method in ruby

I am trying to write a method that takes in an array as an argument and returns an array of the numbers in the argument that are have both an even index number and an even value. I am not sure why, but it is giving me the error "undefined method %" in line 5. Can someone explain how I can fix this?
def odd_value_and_position(array)
newArray=[] #create new array
i=0 #i=0
while i <= array.length #loop while
newArray.push(array[i]) if array[i] % 2 != 0
i = i + 2
end
return newArray
end
puts odd_value_and_position([0,1,2,3,4,5])
Another way to do this:
def evens arr
arr.select.with_index { |e,i| e.even? && i.even? }
end
evens [0,1,2,3,4,5] #=> [0,2,4]
When i is equal to array.length, array[i] is nil.
What is nil % 2? It is undefined.
def odd_value_and_position(array)
newArray=[] #create new array
i=0 #i=0
while i < array.length #loop while
newArray.push(array[i]) if array[i] % 2 != 0
i = i + 2
end
return newArray
end
puts odd_value_and_position([0,1,2,3,4,5]) #=> []
puts odd_value_and_position([1,2,3,4,5]) #=> [1,3,5]
Due to the fact that the first element in a Ruby Array has 0 as index, I'm not sure you get the result you expected. See examples in code.
A more Rubyish example would be :
def odd_value_and_position(array)
array.select.with_index(1){|x,i| x.odd? && i.odd?}
end
puts odd_value_and_position([1,2,3,4,5]) #=> [1,3,5]
If I understand the question right, I'd go with something like:
def some_method_name(array)
array.select.with_index { |*ij|
ij.all?(&:even?)
}
end
puts some_method_name([0, 1, 2, 3, 4, 5, 10, 13, 21, 22, 30])
# >> 0
# >> 2
# >> 4
# >> 10
# >> 30
Here's what it's doing:
def some_method_name(array)
array.select.with_index { |*ij|
ij # => [0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [10, 6], [13, 7], [21, 8], [22, 9], [30, 10]
ij.all?(&:even?) # => true, false, true, false, true, false, true, false, false, false, true
}
end
puts some_method_name([0, 1, 2, 3, 4, 5, 10, 13, 21, 22, 30])
# >> 0
# >> 2
# >> 4
# >> 10
# >> 30
There are a couple problems with the original code.
Using while loops easily leads to problems with off-by-one errors, or loops that never trigger, or loops that never end.
To combat that in Ruby, we use each and map, select, reject or similar iterators to loop over the array, and process each element in turn, then base the logic on that.
array.select is looking at each element and applying the logic in the block, looking for "truthy" results. with_index adds the index of the iteration as a second value passed into the block. *id turns the two values being passed in into an array, making it easy to apply all? and its even? test. If even? returns true to both then all? triggers and returns true again which signals to select to return that element of the array.

Can't iterate over Time objects in Ruby

I am writing an appointment form that will let the user choose a date. It will then take the date and check against a Google Calendar what time slots are available for that date within a range of 30 minutes time intervals from 10:00am to 5:00pm.
Within my Calendar class, I have an available_times method:
def available_times(appointment_date)
appointment_date_events = calendar.events.select { |event| Date.parse(event.start_time) == appointment_date }
conflicts = appointment_date_events.map { |event| [Time.parse(event.start_time), Time.parse(event.end_time)] }
results = resolve_time_conflicts(conflicts)
end
This method takes a date and grabs the start_time and end_time for each event on that date. It then calls resolve_time_conflicts(conflicts):
def resolve_time_conflicts(conflicts)
start_time = Time.parse('10:00am')
available_times = []
14.times do |interval_multiple|
appointment_time = (start_time + interval_multiple * (30 * 60))
available_times << appointment_time unless conflicts.each{ |conflict| (conflict[0]..conflict[1]).include?(appointment_time)}
end
available_times
end
A 'Can't iterate over Time' error is being thrown when I attempt to iterate over the conflicts array. I tried to call to_enum on the conflicts array but am still getting the same error.
All of the other questions I saw on SO were referencing the step method, which doesn't seem applicable to this case.
Update:
Thanks #caryswoveland and #fivedigit. I combined both of your answers, which were very helpful for different aspects of my solution:
def available_times(appointment_date)
appointment_date_events = calendar.events.select { |event| Date.parse(event.start_time) == appointment_date }
conflicts = appointment_date_events.map { |event| DateTime.parse(event.start_time)..DateTime.parse(event.end_time) }
results = resolve_time_conflicts(conflicts)
end
def resolve_time_conflicts(conflicts)
date = conflicts.first.first
start_time = DateTime.new(date.year, date.month, date.day, 10, 00).change(offset: date.zone)
available_times = []
14.times do |interval_multiple|
appointment_time = (start_time + ((interval_multiple * 30).minutes))
available_times << appointment_time unless conflicts.any? { |conflict| conflict.cover?(appointment_time)}
end
available_times
end
Exception
#fivedigit has explained why the exception was raised.
Other problems
You need any? where you have each:
appointment_times = []
#=> []
appointment = 4
#=> 4
conflicts = [(1..3), (5..7)]
#=> [1..3, 5..7]
appointment_times << 5 unless conflicts.each { |r| r.cover?(appointment) }
#=> nil
appointment_times
#=> []
appointment_times << 5 unless conflicts.any? { |r| r.include?(appointment) }
#=> [5]
appointment_times
#=> [5]
I suggest you covert appointment_time to a Time object, make conflicts and array of elements [start_time, end_time] and then compare appointment_time to the endpoints:
...unless conflicts.any?{ |start_time, end_time|
start_time <= appointment_time && appointment_time <= end_time }
Aside: Range#include? only looks at endpoints (as Range#cover? does) when the endpoints are "numeric". Range#include? need only look at endpoints when they are Time objects, but I don't know if Ruby regards Time objects as "numeric". I guess one could look at the source code. Anybody know offhand?
Alternative approach
I would like to suggest a different way to implement your method. I will do so with an example.
Suppose appointments were in blocks of 15 minutes, with the first block being 10:00am-10:15am and the last 4:45pm-5:00pm. (blocks could be shorter, of course, as small as 1 second in duration.)
Let 10:00am-10:15am be block 0, 10:15am-10:30am be block 1, and so on, until block 27, 4:45pm-5:00pm.
Next, express conflicts as an array of block ranges, given by [start, end]. Suppose there were appointments at:
10:45am-11:30am (blocks 3, 4 and 5)
1:00pm- 1:30pm (blocks 12 and 13)
2:15pm- 3:30pm (blocks 17, 18 and 19)
Then:
conflicts = [[3,5], [12,13], [17,19]]
You must write a method reserved_blocks(appointment_date) that returns conflicts.
The remaining code is as follows:
BLOCKS = 28
MINUTES = ["00", "15", "30", "45"]
BLOCK_TO_TIME = (BLOCKS-1).times.map { |i|
"#{i<12 ? 10+i/4 : (i-8)/4}:#{MINUTES[i%4]}#{i<8 ? 'am' : 'pm'}" }
#=> ["10:00am", "10:15am", "10:30am", "10:45am",
# "11:00am", "11:15am", "11:30am", "11:45am",
# "12:00pm", "12:15pm", "12:30pm", "12:45pm",
# "1:00pm", "1:15pm", "1:30pm", "1:45pm",
# "2:00pm", "2:15pm", "2:30pm", "2:45pm",
# "3:00pm", "3:15pm", "3:30pm", "3:45pm",
# "4:00pm", "4:15pm", "4:30pm", "4:45pm"]
def available_times(appointment_date)
available = [*(0..BLOCKS-1)]-reserved_blocks(appointment_date)
.flat_map { |s,e| (s..e).to_a }
last = -2 # any value will do, can even remove statement
test = false
available.chunk { |b| (test=!test) if b > last+1; last = b; test }
.map { |_,a| [BLOCK_TO_TIME[a.first],
(a.last < BLOCKS-1) ? BLOCK_TO_TIME[a.last+1] : "5:00pm"] }
end
def reserved_blocks(date) # stub for demonstration.
[[3,5], [12,13], [17,19]]
end
Let's see what we get:
available_times("anything")
#=> [["10:00am", "10:45am"],
# ["11:30am", "1:00pm"],
# [ "1:45pm", "2:15pm"],
# [ "3:00pm", "5:00pm"]]
Explanation
Here is what's happening:
appointment_date = "anything" # dummy for demonstration
all_blocks = [*(0..BLOCKS-1)]
#=> [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
# 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
reserved_ranges = reserved_blocks(appointment_date)
#=> [[3, 5], [12, 13], [17, 19]]
reserved = reserved_ranges.flat_map { |s,e| (s..e).to_a }
#=> [3, 4, 5, 12, 13, 17, 18, 19]
available = ALL_BLOCKS - reserved
#=> [0, 1, 2, 6, 7, 8, 9, 10, 11, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27]
last = -2
test = false
enum1 = available.chunk { |b| (test=!test) if b > last+1; last = b; test }
#=> #<Enumerator: #<Enumerator::Generator:0x00000103063570>:each>
We can convert it to an array to see what values it would pass into the block if map did not follow:
enum1.to_a
#=> [[true, [0, 1, 2]],
# [false, [6, 7, 8, 9, 10, 11]],
# [true, [14, 15, 16]],
# [false, [20, 21, 22, 23, 24, 25, 26, 27]]]
Enumerable#chunk groups consecutive values of the enumerator. It does so by grouping on the value of test and flipping its value between true and false whenever a non-consecutive value is encountered.
enum2 = enum1.map
#=> #<Enumerator: #<Enumerator: (cont.)
#<Enumerator::Generator:0x00000103063570>:each>:map>
enum2.to_a
#=> [[true, [0, 1, 2]],
# [false, [6, 7, 8, 9, 10, 11]],
# [true, [14, 15, 16]],
# [false, [20, 21, 22, 23, 24, 25, 26, 27]]]
You might think of enum2 as a "compound" enumerator.
Lastly, we convert the second element of each value of enum2 that is passed into the block (the block variable a, which equals [0,1,2] for the first element passed) to a range expressed as a 12-hour time. The first element of each value of enum2 (true or false) is not used, so so I've replaced its block variable with an underscore. This provides the desired result:
enum2.each { |_,a|[BLOCK_TO_TIME[a.first], \
(a.last < BLOCKS-1) ? BLOCK_TO_TIME[a.last+1] : "5:00pm"] }
#=> [["10:00am", "10:45am"],
# ["11:30am", "1:00pm"],
# [ "1:45pm", "2:15pm"],
# [ "3:00pm", "5:00pm"]]
The issue comes from this bit:
(conflict[0]..conflict[1]).include?(appointment_time)
# TypeError: can't iterate from Time
You're creating a range of times and then checking if appointment_time falls within the range. This is what causes the error you're experiencing.
Instead of include?, you should use cover?:
(conflict[0]..conflict[1]).cover?(appointment_time)
This assumes that conflict[0] is the earliest time.
Convert your range from a range of times to a range of integers:
range = (conflict[0].to_i..conflict[1].to_i)
Then use the === operator as you used the include?:
conflict === appointment_time
EDIT: You can also obviously convert appointment_time to integer and still use include? since the range is now just an integer range.

Every Other 2 Items in Array

I need a ruby formula to create an array of integers. The array must be every other 2 numbers as follows.
[2, 3, 6, 7, 10, 11, 14, 15, 18, 19...]
I have read a lot about how I can do every other number or multiples, but I am not sure of the best way to achieve what I need.
Here's an approach that works on any array.
def every_other_two arr
arr.select.with_index do |_, idx|
idx % 4 > 1
end
end
every_other_two((0...20).to_a) # => [2, 3, 6, 7, 10, 11, 14, 15, 18, 19]
# it works on any array
every_other_two %w{one two three four five six} # => ["three", "four"]
array = []
#Change 100000 to whatever is your upper limit
100000.times do |i|
array << i if i%4 > 1
end
This code works for any start number to any end limit
i = 3
j = 19
x =[]
(i...j).each do |y|
x << y if (y-i)%4<2
end
puts x
this should work
For fun, using lazy enumerables (requires Ruby 2.0 or gem enumerable-lazy):
(2..Float::INFINITY).step(4).lazy.map(&:to_i).flat_map { |x| [x, x+1] }.first(8)
#=> => [2, 3, 6, 7, 10, 11, 14, 15]
here's a solution that works with infinite streams:
enum = Enumerator.new do |y|
(2...1/0.0).each_slice(4) do |slice|
slice[0 .. 1].each { |n| y.yield(n) }
end
end
enum.first(10) #=> [2, 3, 6, 7, 10, 11, 14, 15, 18, 19]
enum.each do |n|
puts n
end
Single Liner:
(0..20).to_a.reduce([0,[]]){|(count,arr),ele| arr << ele if count%4 > 1;
[count+1,arr] }.last
Explanation:
Starts the reduce look with 0,[] in count,arr vars
Add current element to array if condition satisfied. Block returns increment and arr for the next iteration.
I agree though that it is not so much of a single liner though and a bit complex looking.
Here's a slightly more general version of Sergio's fine answer
module Enumerable
def every_other(slice=1)
mod = slice*2
res = select.with_index { |_, i| i % mod >= slice }
block_given? ? res.map{|x| yield(x)} : res
end
end
irb> (0...20).every_other
=> [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
irb> (0...20).every_other(2)
=> [2, 3, 6, 7, 10, 11, 14, 15, 18, 19]
irb> (0...20).every_other(3)
=> [3, 4, 5, 9, 10, 11, 15, 16, 17]
irb> (0...20).every_other(5) {|v| v*10 }
=> [50, 60, 70, 80, 90, 150, 160, 170, 180, 190]

Resources