Optimization for finding perfect-square algorithm - ruby

The question I'm working on is:
Find which sum of squared factors are a perfect square given a specific range.
So if the range was (1..10) you would get each number's factors (all factors for 1, all factors for 2, all factors for 3 ect..) Square those factors, then add them together. Finally check if that sum is a perfect square.
I am stuck on refactoring/optimization because my solution is too slow.
Here is what I came up with:
def list_squared(m, n)
ans = []
range = (m..n)
range.each do |i|
factors = (1..i).select { |j| i % j == 0 }
squares = factors.map { |k| k ** 2 }
sum = squares.inject { |sum,x| sum + x }
if sum == Math.sqrt(sum).floor ** 2
all = []
all += [i, sum]
ans << all
end
end
ans
end
This is an example of what I would put in the method:
list_squared(1, 250)
And then the desired output would be an array of arrays with each array containing the number whose sum of squared factors was a perfect square and the sum of those squared factors:
[[1, 1], [42, 2500], [246, 84100]]

I would start by introducing some helper methods (factors and square?) to make your code more readable.
Furthermore, I would reduce the number of ranges and arrays to improve memory usage.
require 'prime'
def factors(number)
[1].tap do |factors|
primes = number.prime_division.flat_map { |p, e| Array.new(e, p) }
(1..primes.size).each do |i|
primes.combination(i).each do |combination|
factor = combination.inject(:*)
factors << factor unless factors.include?(factor)
end
end
end
end
def square?(number)
square = Math.sqrt(number)
square == square.floor
end
def list_squared(m, n)
(m..n).map do |number|
sum = factors(number).inject { |sum, x| sum + x ** 2 }
[number, sum] if square?(sum)
end.compact
end
list_squared(1, 250)
A benchmark with a narrow range (up to 250) shows only a minor improvement:
require 'benchmark'
n = 1_000
Benchmark.bmbm(15) do |x|
x.report("original_list_squared :") { n.times do; original_list_squared(1, 250); end }
x.report("improved_list_squared :") { n.times do; improved_list_squared(1, 250); end }
end
# Rehearsal -----------------------------------------------------------
# original_list_squared : 2.720000 0.010000 2.730000 ( 2.741434)
# improved_list_squared : 2.590000 0.000000 2.590000 ( 2.604415)
# -------------------------------------------------- total: 5.320000sec
# user system total real
# original_list_squared : 2.710000 0.000000 2.710000 ( 2.721530)
# improved_list_squared : 2.620000 0.010000 2.630000 ( 2.638833)
But a benchmark with a wider range (up to 10000) shows a much better performance than the original implementation:
require 'benchmark'
n = 10
Benchmark.bmbm(15) do |x|
x.report("original_list_squared :") { n.times do; original_list_squared(1, 10000); end }
x.report("improved_list_squared :") { n.times do; improved_list_squared(1, 10000); end }
end
# Rehearsal -----------------------------------------------------------
# original_list_squared : 36.400000 0.160000 36.560000 ( 36.860889)
# improved_list_squared : 2.530000 0.000000 2.530000 ( 2.540743)
# ------------------------------------------------- total: 39.090000sec
# user system total real
# original_list_squared : 36.370000 0.120000 36.490000 ( 36.594130)
# improved_list_squared : 2.560000 0.010000 2.570000 ( 2.581622)
tl;dr: The bigger the N the better my code performs compared to the original implementation...

One way to make it more efficient is to use Ruby's built-in method Prime::prime_division.
For any number n, if prime_division returns an array containing a single element, that element will be [n,1] and n will have been shown to be prime. That prime number has factors n and 1, so must be treated differently than numbers that are not prime.
require 'prime'
def list_squared(range)
range.each_with_object({}) do |i,h|
facs = Prime.prime_division(i)
ssq =
case facs.size
when 1 then facs.first.first**2 + 1
else facs.inject(0) { |tot,(a,b)| tot + b*(a**2) }
end
h[i] = facs if (Math.sqrt(ssq).to_i)**2 == ssq
end
end
list_squared(1..10_000)
#=> { 1=>[], 48=>[[2, 4], [3, 1]], 320=>[[2, 6], [5, 1]], 351=>[[3, 3], [13, 1]],
# 486=>[[2, 1], [3, 5]], 1080=>[[2, 3], [3, 3], [5, 1]],
# 1260=>[[2, 2], [3, 2], [5, 1], [7, 1]], 1350=>[[2, 1], [3, 3], [5, 2]],
# 1375=>[[5, 3], [11, 1]], 1792=>[[2, 8], [7, 1]], 1836=>[[2, 2], [3, 3], [17, 1]],
# 2070=>[[2, 1], [3, 2], [5, 1], [23, 1]], 2145=>[[3, 1], [5, 1], [11, 1], [13, 1]],
# 2175=>[[3, 1], [5, 2], [29, 1]], 2730=>[[2, 1], [3, 1], [5, 1], [7, 1], [13, 1]],
# 2772=>[[2, 2], [3, 2], [7, 1], [11, 1]], 3072=>[[2, 10], [3, 1]],
# 3150=>[[2, 1], [3, 2], [5, 2], [7, 1]], 3510=>[[2, 1], [3, 3], [5, 1], [13, 1]],
# 4104=>[[2, 3], [3, 3], [19, 1]], 4305=>[[3, 1], [5, 1], [7, 1], [41, 1]],
# 4625=>[[5, 3], [37, 1]], 4650=>[[2, 1], [3, 1], [5, 2], [31, 1]],
# 4655=>[[5, 1], [7, 2], [19, 1]], 4998=>[[2, 1], [3, 1], [7, 2], [17, 1]],
# 5880=>[[2, 3], [3, 1], [5, 1], [7, 2]], 6000=>[[2, 4], [3, 1], [5, 3]],
# 6174=>[[2, 1], [3, 2], [7, 3]], 6545=>[[5, 1], [7, 1], [11, 1], [17, 1]],
# 7098=>[[2, 1], [3, 1], [7, 1], [13, 2]], 7128=>[[2, 3], [3, 4], [11, 1]],
# 7182=>[[2, 1], [3, 3], [7, 1], [19, 1]], 7650=>[[2, 1], [3, 2], [5, 2], [17, 1]],
# 7791=>[[3, 1], [7, 2], [53, 1]], 7889=>[[7, 3], [23, 1]],
# 7956=>[[2, 2], [3, 2], [13, 1], [17, 1]],
# 9030=>[[2, 1], [3, 1], [5, 1], [7, 1], [43, 1]],
# 9108=>[[2, 2], [3, 2], [11, 1], [23, 1]], 9295=>[[5, 1], [11, 1], [13, 2]],
# 9324=>[[2, 2], [3, 2], [7, 1], [37, 1]]}
This calculation took approximately 0.15 seconds.
For i = 6174
(2**1) * (3**2) * (7**3) #=> 6174
and
1*(2**2) + 2*(3**2) + 3*(7**2) #=> 169 == 13*13

The trick that frequently solves questions like this is to switch from trial division to a sieve. In Python (sorry):
def list_squared(m, n):
factor_squared_sum = {i: 0 for i in range(m, n + 1)}
for factor in range(1, n + 1):
i = n - n % factor # greatest multiple of factor less than or equal to n
while i >= m:
factor_squared_sum[i] += factor ** 2
i -= factor
return {i for (i, fss) in factor_squared_sum.items() if isqrt(fss) ** 2 == fss}
def isqrt(n):
# from http://stackoverflow.com/a/15391420
x = n
y = (x + 1) // 2
while y < x:
x = y
y = (x + n // x) // 2
return x
The next optimization is to step factor only to isqrt(n), adding the factor squares in pairs (e.g., 2 and i // 2).

Related

Ruby Solution for codewars sum of intervals 4kyu doesnot work

[Sum of intervals] (https://www.codewars.com/kata/52b7ed099cdc285c300001cd/ruby)
My solution for this kyu
def sum_of_intervals(intervals)
intervals.uniq.sort_by!(&:last)
sum = 0
new_intervals = intervals.sort_by(&:first).each_with_object([intervals.first]) do |interval, arr|
if interval.first <= arr.last.last
arr[-1] = arr.last.first, [arr.last.last, interval.last].max
else
arr << interval
end
end
new_intervals.each do |interval|
sum += (interval[1] - interval[0])
end
p sum
end
After writing code we have two options - test and attempt
My def pass successfully with test and failed with attempt
I cannot see test for attempt
May be sombody could teke a look what`s wrong with my code?
Thanks a lot
intervals.uniq.sort_by!(&:last)
This almost certainly doesn't do what you think it does. Consider:
irb(main):006:0> a = [[1, 2], [3, 4], [5, 0], [1, 2]]
irb(main):007:0> a.uniq.sort_by!(&:last)
=> [[5, 0], [1, 2], [3, 4]]
irb(main):008:0> a
=> [[1, 2], [3, 4], [5, 0], [1, 2]]
irb(main):009:0> (b = a.uniq).sort_by!(&:last)
=> [[5, 0], [1, 2], [3, 4]]
irb(main):010:0> a
=> [[1, 2], [3, 4], [5, 0], [1, 2]]
irb(main):011:0> b
=> [[5, 0], [1, 2], [3, 4]]
intervals.uniq is creating a new array, which #sort_by! does sort destructively, but that does not affect intervals.
You can use the destructive #uniq! in this case, but that method will return nil if the array is already "unique", leading to an exception when you try to call #sort_by! on nil. Using &. (intervals.uniq!&.sort_by!(&:last)) will prevent the exception, but may leave your data unsorted.
You may be better served by the much simpler:
intervals = intervals.uniq.sort_by(&:last)
Though Chris has answered your question, I would like to suggest an alternative solution.
First define a helper method, where the argument r is a range.
def completed_range_span(r)
r.end - r.begin
end
Now define the main method.
def total_arr_lengths(arr)
# convert arr to an array of ranges ordered by beginning of range
a = arr.map { |e| e.first..e.last }.sort_by(&:begin)
tot = 0
loop do
# If a contains only a single range add the span of that range to tot,
# after which we are finished
break (tot + completed_range_span(a.first)) if a.size == 1
# We're not finished
# For readability, assign first two elements of a to variables
r0 = a[0]
r1 = a[1]
# If r0 and r1 do not overlap add the span of r0 to tot
# else alter r1 to be the range formed by r0 and r1
if r0.end < r1.begin
tot += completed_range_span(r0)
else
a[1]= r0.begin..[r0.end, r1.end].max
end
# remove r0
a.shift
end
end
Let's try it.
total_arr_lengths [[1,4], [7, 10], [3, 5]] #=> 7
total_arr_lengths [[1,2], [6, 10], [11, 15]] #=> 9
total_arr_lengths [[1,4], [7, 10], [3, 5]] #=> 7
total_arr_lengths [[1,5], [10, 20], [1, 6], [16, 19], [5, 11]] #=> 19
total_arr_lengths [[0, 20], [-100000000, 10], [30, 40]] #=> 100000030
To help the reader confirm the results for these examples, for each argument (an array) I have displayed below the corresponding value of the array of ordered ranges obtained by the first calculation performed by the main method:
arr.map { |e| e.first..e.last }.sort_by(&:begin)
arr array of ordered ranges
-------------------------------------------- -----------------------------------
[[1,4], [7, 10], [3, 5]] [1..4, 3..5, 7..10]
[[1,2], [6, 10], [11, 15]] [1..2, 6..10, 11..15]
[[1,4], [7, 10], [3, 5]] [1..4, 3..5, 7..10]
[[1,5], [10, 20], [1, 6], [16, 19], [5, 11]] [1..5, 1..6, 5..11, 10..20, 16..19]
[[0, 20], [-100000000, 10], [30, 40]] [-100000000..10, 0..20, 30..40]
I converted the arrays to ranges to improve readability (in my opinion). I don't expect it affects computational efficiency, though it generally saves some memory.

Rolling average in Ruby?

I would like to do rolling average on data, x and y values.
e.g.
rolling_average([[0, 1], [1, 2], [2, 3]])
would give me
[[0, 2.0], [1, 2.5], [2, 3.0]]
I couldn't find anywhere how to do it
I believe rolling average (a.ka. moving average) is a misnomer here, but the desired calculation could be performed as follows.
def rolling_average(data)
tot = 0
sz = data.size
data.reverse_each.with_object([]) do |(i,n),a|
tot += n
a.unshift([i, tot.fdiv(sz-i)])
end
end
rolling_average [[0, 1], [1, 2], [2, 3]]
#=> [[0, 2.0], [1, 2.5], [2, 3.0]]
data = 10.times.map { rand(10) }
#=> [[0, 5], [1, 7], [2, 9], [3, 4], [4, 0],
# [5, 5], [6, 1], [7, 6], [8, 3], [9, 5]]
rolling_average data # to 3 decimal places
#=>[[0, 4.5], [1, 4.444], [2, 4.125], [3, 3.428], [4, 3.333],
# [5, 4.0], [6, 3.75], [7, 4.666], [8, 4.0], [9, 5.0]]
Keeping a running total avoids the need to perform a complete summation for each element in data.
Though ever-so-slightly less efficient, it may be clear to write the following.
def rolling_average(data)
tot = data.sum(&:last)
sz = data.size
data.map do |i,n|
avg = tot.fdiv(sz-i)
tot -= n
[i, avg]
end
end
To compute a rolling average of size avg_size one could compute the following, where data is simply an array of values to be averaged.
def rolling_average(data, avg_size)
raise ArgumentError if data.size < avg_size
block = data.first(avg_size)
tot = block.sum
sz = data.size
(avg_size..sz).map do |i|
avg = tot.fdiv(avg_size)
if i < sz
x = data[i]
tot += x - block.first
(block << x).shift
end
avg
end
end
data = [5, 7, 9, 4, 0, 5, 1, 6, 3, 5]
rolling_average(data, 3)
#=> [7.0, 6.667, 4.333, 3.0, 2.0, 4.0, 3.333, 4.667]
The first element of the array returned by rolling_average equals (5 + 7 + 9)/3.0 #=> 7.0, the second equals (7 + 9 + 4)/3.0 #=> 6.667, and so on.
Here's what I did:
def rolling_average(data, size = 10)
data.map.with_index do |_, i|
[
data[i].first,
data[i..(i + size)].map(&:last).then { |a| a.sum.to_f / a.size }
]
end
end

Integer to array of chunks

I need to turn an integer into an array of intervals. For example,
number_to_steps(number: 10, step: 3)
# => [[0, 2], [3, 5], [6, 8], [9, 9]]
number_to_steps(number: 7, step: 2)
# => [[0, 1], [2, 3], [4, 5], [6, 6]]
number_to_steps(number: 8, step: 2)
# => [[0, 1], [2, 3], [4, 5], [6, 7]]
I tried:
def number_to_ranges(number:, size:)
chunks = ((number - 1) / size.to_f).ceil
(0..chunks - 1).map do |index|
from = index * size
to = (index + 1) * size - 1
[ from, to > number ? number : to ]
end
end
But it doesn't work properly. For example,
number_to_ranges(number: 14, step: 4)
[[0, 3], [4, 7], [8, 11], [12, 14]]
should not go to 14.
Any idea?
An alternative (to your method) would be to use each_slice.
def number_to_steps(number:, step:)
(0...number).each_slice(step).map { |arr| [arr.first, arr.last] }
end
Tests:
number_to_steps(number: 10, step: 3)
#=> [[0, 2], [3, 5], [6, 8], [9, 9]]
number_to_steps(number: 7, step: 2)
#=> [[0, 1], [2, 3], [4, 5], [6, 6]]
number_to_steps(number: 8, step: 2)
#=> [[0, 1], [2, 3], [4, 5], [6, 7]]
def number_to_steps(number:, size:)
(0..number-1).step(size).map { |i| [i, [i+size, number].min-1] }
end
number_to_steps(number: 10, size: 3)
#=> [[0, 2], [3, 5], [6, 8], [9, 9]]
number_to_steps(number: 7, size: 2)
#=> [[0, 1], [2, 3], [4, 5], [6, 6]]
number_to_steps(number: 8, size: 2)
#=> [[0, 1], [2, 3], [4, 5], [6, 7]]
There are two places where you need to change your method number_to_ranges.
When calculating the number of chunks, change number - 1 to number.
When finding the last element for a chunk, do number - 1 instead of number.
This is what the final code should look like:
def number_to_ranges(number:, size:)
chunks = (number / size.to_f).ceil
(0...chunks).map do |index|
from = index * size
to = (index + 1) * size - 1
[from, [to, number - 1].min]
end
end
number_to_ranges(number: 10, size: 3)
=> [[0, 2], [3, 5], [6, 8], [9, 9]]
number_to_ranges(number: 7, size: 2)
=> [[0, 1], [2, 3], [4, 5], [6, 6]]
number_to_ranges(number: 8, size: 2)
=> [[0, 1], [2, 3], [4, 5], [6, 7]]
number_to_ranges(number: 14, size: 4)
=> [[0, 3], [4, 7], [8, 11], [12, 13]]

Algorithmic Optimization [duplicate]

The question I'm working on is:
Find which sum of squared factors are a perfect square given a specific range.
So if the range was (1..10) you would get each number's factors (all factors for 1, all factors for 2, all factors for 3 ect..) Square those factors, then add them together. Finally check if that sum is a perfect square.
I am stuck on refactoring/optimization because my solution is too slow.
Here is what I came up with:
def list_squared(m, n)
ans = []
range = (m..n)
range.each do |i|
factors = (1..i).select { |j| i % j == 0 }
squares = factors.map { |k| k ** 2 }
sum = squares.inject { |sum,x| sum + x }
if sum == Math.sqrt(sum).floor ** 2
all = []
all += [i, sum]
ans << all
end
end
ans
end
This is an example of what I would put in the method:
list_squared(1, 250)
And then the desired output would be an array of arrays with each array containing the number whose sum of squared factors was a perfect square and the sum of those squared factors:
[[1, 1], [42, 2500], [246, 84100]]
I would start by introducing some helper methods (factors and square?) to make your code more readable.
Furthermore, I would reduce the number of ranges and arrays to improve memory usage.
require 'prime'
def factors(number)
[1].tap do |factors|
primes = number.prime_division.flat_map { |p, e| Array.new(e, p) }
(1..primes.size).each do |i|
primes.combination(i).each do |combination|
factor = combination.inject(:*)
factors << factor unless factors.include?(factor)
end
end
end
end
def square?(number)
square = Math.sqrt(number)
square == square.floor
end
def list_squared(m, n)
(m..n).map do |number|
sum = factors(number).inject { |sum, x| sum + x ** 2 }
[number, sum] if square?(sum)
end.compact
end
list_squared(1, 250)
A benchmark with a narrow range (up to 250) shows only a minor improvement:
require 'benchmark'
n = 1_000
Benchmark.bmbm(15) do |x|
x.report("original_list_squared :") { n.times do; original_list_squared(1, 250); end }
x.report("improved_list_squared :") { n.times do; improved_list_squared(1, 250); end }
end
# Rehearsal -----------------------------------------------------------
# original_list_squared : 2.720000 0.010000 2.730000 ( 2.741434)
# improved_list_squared : 2.590000 0.000000 2.590000 ( 2.604415)
# -------------------------------------------------- total: 5.320000sec
# user system total real
# original_list_squared : 2.710000 0.000000 2.710000 ( 2.721530)
# improved_list_squared : 2.620000 0.010000 2.630000 ( 2.638833)
But a benchmark with a wider range (up to 10000) shows a much better performance than the original implementation:
require 'benchmark'
n = 10
Benchmark.bmbm(15) do |x|
x.report("original_list_squared :") { n.times do; original_list_squared(1, 10000); end }
x.report("improved_list_squared :") { n.times do; improved_list_squared(1, 10000); end }
end
# Rehearsal -----------------------------------------------------------
# original_list_squared : 36.400000 0.160000 36.560000 ( 36.860889)
# improved_list_squared : 2.530000 0.000000 2.530000 ( 2.540743)
# ------------------------------------------------- total: 39.090000sec
# user system total real
# original_list_squared : 36.370000 0.120000 36.490000 ( 36.594130)
# improved_list_squared : 2.560000 0.010000 2.570000 ( 2.581622)
tl;dr: The bigger the N the better my code performs compared to the original implementation...
One way to make it more efficient is to use Ruby's built-in method Prime::prime_division.
For any number n, if prime_division returns an array containing a single element, that element will be [n,1] and n will have been shown to be prime. That prime number has factors n and 1, so must be treated differently than numbers that are not prime.
require 'prime'
def list_squared(range)
range.each_with_object({}) do |i,h|
facs = Prime.prime_division(i)
ssq =
case facs.size
when 1 then facs.first.first**2 + 1
else facs.inject(0) { |tot,(a,b)| tot + b*(a**2) }
end
h[i] = facs if (Math.sqrt(ssq).to_i)**2 == ssq
end
end
list_squared(1..10_000)
#=> { 1=>[], 48=>[[2, 4], [3, 1]], 320=>[[2, 6], [5, 1]], 351=>[[3, 3], [13, 1]],
# 486=>[[2, 1], [3, 5]], 1080=>[[2, 3], [3, 3], [5, 1]],
# 1260=>[[2, 2], [3, 2], [5, 1], [7, 1]], 1350=>[[2, 1], [3, 3], [5, 2]],
# 1375=>[[5, 3], [11, 1]], 1792=>[[2, 8], [7, 1]], 1836=>[[2, 2], [3, 3], [17, 1]],
# 2070=>[[2, 1], [3, 2], [5, 1], [23, 1]], 2145=>[[3, 1], [5, 1], [11, 1], [13, 1]],
# 2175=>[[3, 1], [5, 2], [29, 1]], 2730=>[[2, 1], [3, 1], [5, 1], [7, 1], [13, 1]],
# 2772=>[[2, 2], [3, 2], [7, 1], [11, 1]], 3072=>[[2, 10], [3, 1]],
# 3150=>[[2, 1], [3, 2], [5, 2], [7, 1]], 3510=>[[2, 1], [3, 3], [5, 1], [13, 1]],
# 4104=>[[2, 3], [3, 3], [19, 1]], 4305=>[[3, 1], [5, 1], [7, 1], [41, 1]],
# 4625=>[[5, 3], [37, 1]], 4650=>[[2, 1], [3, 1], [5, 2], [31, 1]],
# 4655=>[[5, 1], [7, 2], [19, 1]], 4998=>[[2, 1], [3, 1], [7, 2], [17, 1]],
# 5880=>[[2, 3], [3, 1], [5, 1], [7, 2]], 6000=>[[2, 4], [3, 1], [5, 3]],
# 6174=>[[2, 1], [3, 2], [7, 3]], 6545=>[[5, 1], [7, 1], [11, 1], [17, 1]],
# 7098=>[[2, 1], [3, 1], [7, 1], [13, 2]], 7128=>[[2, 3], [3, 4], [11, 1]],
# 7182=>[[2, 1], [3, 3], [7, 1], [19, 1]], 7650=>[[2, 1], [3, 2], [5, 2], [17, 1]],
# 7791=>[[3, 1], [7, 2], [53, 1]], 7889=>[[7, 3], [23, 1]],
# 7956=>[[2, 2], [3, 2], [13, 1], [17, 1]],
# 9030=>[[2, 1], [3, 1], [5, 1], [7, 1], [43, 1]],
# 9108=>[[2, 2], [3, 2], [11, 1], [23, 1]], 9295=>[[5, 1], [11, 1], [13, 2]],
# 9324=>[[2, 2], [3, 2], [7, 1], [37, 1]]}
This calculation took approximately 0.15 seconds.
For i = 6174
(2**1) * (3**2) * (7**3) #=> 6174
and
1*(2**2) + 2*(3**2) + 3*(7**2) #=> 169 == 13*13
The trick that frequently solves questions like this is to switch from trial division to a sieve. In Python (sorry):
def list_squared(m, n):
factor_squared_sum = {i: 0 for i in range(m, n + 1)}
for factor in range(1, n + 1):
i = n - n % factor # greatest multiple of factor less than or equal to n
while i >= m:
factor_squared_sum[i] += factor ** 2
i -= factor
return {i for (i, fss) in factor_squared_sum.items() if isqrt(fss) ** 2 == fss}
def isqrt(n):
# from http://stackoverflow.com/a/15391420
x = n
y = (x + 1) // 2
while y < x:
x = y
y = (x + n // x) // 2
return x
The next optimization is to step factor only to isqrt(n), adding the factor squares in pairs (e.g., 2 and i // 2).

Fill sparse array

I have a sparse array, for example:
rare = [[0,1], [2,3], [4,5], [7,8]]
I want to plot a chart with these data, each pair are point coordinates.
As you can see I don't have points for x=1, x=3 , x=5, x=6
I want to fill the array with the previous values, so for the above example I will get:
filled = [[0,1], [1,1], [2,3], [3,3], [4,5], [5,5], [6,5], [7,8]
As you can see, for calculating the y value, I simply take the last y value I used.
What is the best aproach to accomplish this ?
Range.new(*rare.transpose.first.sort.values_at(0,-1)).inject([]){|a,i|
a<<[i, Hash[rare][i] || a.last.last]
}
Step-by-step explanation:
rare.transpose.first.sort.values_at(0,-1) finds min and max x ([0,7] in your example)
Range.new() makes a range out of it (0..7)
inject iterates through the range and for every x returns pair [x,y], where y is:
y from input array, where defined
y from previously evaluated pair, where not
Note: here are some other ways of finding min and max x:
[:min,:max].map{|m| Hash[rare].keys.send m}
rare.map{|el| el.first}.minmax # Ruby 1.9, by steenslag
rare = [[0,1], [2,3], [4,5], [7,8]]
filled = rare.inject([]) do |filled, point|
extras = if filled.empty?
[]
else
(filled.last[0] + 1 ... point[0]).collect do |x|
[x, filled.last[1]]
end
end
filled + extras + [point]
end
p filled
# => [[0, 1], [1, 1], [2, 3], [3, 3], [4, 5], [5, 5], [6, 5], [7, 8]]
An inject solution:
filled = rare.inject([]) do |filled_acc, (pair_x, pair_y)|
padded_pairs = unless filled_acc.empty?
last_x, last_y = filled_acc.last
(last_x+1...pair_x).map { |x| [x, last_y] }
end || []
filled_acc + padded_pairs + [[pair_x, pair_y]]
end
More about Enumerable#inject and functional programming with Ruby here.
irb(main):001:0> rare = [[0,1], [2,3], [4,5], [7,8]]
=> [[0, 1], [2, 3], [4, 5], [7, 8]]
irb(main):002:0> r=rare.transpose
=> [[0, 2, 4, 7], [1, 3, 5, 8]]
irb(main):003:0> iv = (r[0][0]..r[0][-1]).to_a.select {|w| !r[0].include?(w) }
=> [1, 3, 5, 6]
irb(main):004:0> r[1][-1]=r[1][-2]
=> 5
irb(main):005:0> p (iv.zip(r[1]) + rare).sort
[[0, 1], [1, 1], [2, 3], [3, 3], [4, 5], [5, 5], [6, 5], [7, 8]]
=> [[0, 1], [1, 1], [2, 3], [3, 3], [4, 5], [5, 5], [6, 5], [7, 8]]

Resources