Currently I have a problem with synchronizing time periods in Ruby on Rails.
I have 2 arrays of none to n hashes that I need to compare and then have synchronous time periods in a "result" array.
As an example:
time_periods_a = [
{ start_date: '01/10/2021', end_date: '31/10/2021',
additional_attribute: 5 },
{ start_date: '01/11/2021', end_date: '30/11/2021',
additional_attribute: 10 }
]
time_periods_b = [
{ start_date: '01/10/2021', end_date: '31/12/2021',
additional_attribute: 20 }
The result should be:
[
{ start_date: '01/10/2021', end_date: '31/10/2021',
additional_attribute_a: 5, additional_attribute_b: 20 },
{ start_date: '01/11/2021', end_date: '30/11/2021',
additional_attribute_a: 10, additional_attribute_b: 20 },
{ start_date: '01/12/2021', end_date: '31/12/2021',
additional_attribute_a: 0, additional_attribute_b: 20 }
]
Maybe you have a simple solution for this, I despair already for 2 days.
EDIT:
Since i didnt explained the Question well enough, here is an edit for it:
The two arrays may contain hashes that overlap. Generally, these must then be split into several periods.
Maybe another example, a little shorter but more complicated.
time_periods_a: [1 - 2, 3 - 6, 7 - 9]
time_periods_b: [ 2-5, 6-9]
This needs to result in: [1-1, 2-2, 3-5, 6-6, 7-9]
This numbers are just the month index, because the start_date is at the begin of the month every time and the end_date at the end of the month.
It's not required that both arrays contains the same periods, but its possible.
That is why i added the additional attribute.
If one time period is covered in 1 array but not in the other, then i need to add this period with just an additional_attribute: 0
EDIT #2 CODE I USED BEFORE
def format_date_ranges(example_model)
first_array = example_model.first_array
second_array = example_model.second_array
return first_array.map { |hash| hash.merge('additional_attribute_b' => hash['additional_attribute'], 'additional_attribute_b' => 0) } if second_array.blank?
return first_array.map { |hash| hash.merge('additional_attribute_a' => hash['additional_attribute'], 'additional_attribute_b' => 0) } if second_array.blank?
iterate_count = [first_array.size, second_array.size].max
result_array = []
iterate_count.times do |index|
relevant_first_hash = first_hash[index]
releveant_second_hash = second_hash[index]
if relevant_first_hash.nil?
result_array << result_hash(0,
releveant_second_hash['additional_attribute'],
releveant_second_hash['start_date'],
releveant_second_hash['end_date'],
'')
next
end
if relevant_second_hash.nil?
result_array << result_hash(relevant_first_hash['additional_attribute'],
0,
relevant_first_hash['start_date'],
relevant_first_hash['end_date'],
relevant_first_hash['interval'])
next
end
first_start_date = relevant_first_hash['start_date'].to_date
first_end_date = relevant_first_hash['end_date'].to_date
second_start_date = relevant_second_hash['start_date'].to_date
second_end_date = relevant_second_hash['end_date'].to_date
if first_start_date == second_start_date
start_date = second_start_date
else
start_date = [first_start_date, second_start_date].min
end_date = [first_start_date, second_start_date].max - 1.day
result_array << result_hash(relevant_first_hash['additional_attribute'],
relevant_second_hash['additional_attribute'],
start_date,
end_date,
relevant_first_hash['interval'])
start_date = [first_start_date, second_start_date].max
end
if first_end_date == second_end_date
end_date = second_end_date
else
end_date = [first_end_date, second_end_date].min
result_array << result_hash(relevant_first_hash['additional_attribute'],
relevant_second_hash['additional_attribute'],
start_date,
end_date,
relevant_first_hash['interval'])
start_date = end_date 1.day
end_date = [first_end_date, second_end_date].max
end
result_array << result_hash(relevant_first_hash['additional_attribute'],
relevant_second_hash['additional_attribute'],
start_date,
end_date,
relevant_first_hash['interval'])
end
result_array
end
def result_hash(additional_attribute_a, additional_attribute_b, start_date, end_date, interval)
{ additional_attribute_a: additional_attribute_a, additional_attribute_b: additional_attribute_b, start_date: start_date, end_date: end_date, interval: interval }
end
CodePudding user response:
I don't think there is any way around the complexity of the problem short of re-thinking your data structures, something you might consider doing. I have tried to explain most of the steps below but you may have to run the code with some puts
statements salted in to obtain a thorough understanding.
I have generalized the problem into one having any number of arrays of hashes, which actually simplifies the code.
Given data and generalization
a = [
{ start_date: '01/10/2021', end_date: '31/10/2021',
additional_attribute: 5 },
{ start_date: '01/11/2021', end_date: '30/11/2021',
additional_attribute: 10 }
]
b = [
{ start_date: '01/10/2021', end_date: '31/12/2021',
additional_attribute: 20 }
]
Let
array = [a, b]
#=> [[{:start_date=>"01/10/2021", :end_date=>"31/10/2021",
# :additional_attribute=>5},
# {:start_date=>"01/11/2021", :end_date=>"30/11/2021",
# :additional_attribute=>10}],
# [{:start_date=>"01/10/2021", :end_date=>"31/12/2021",
# :additional_attribute=>20}]]
Create two helper methods
def date_str_to_date(date_str)
DateTime.strptime(date_str, '%d/%m/%Y')
end
def date_to_date_str(date_time)
date_time.strftime('%d/%m/%Y')
end
For example:
date_str_to_date('01/10/2021')
#=> #<DateTime: 2021-10-01T00:00:00 00:00 ((2459489j,0s,0n), 0s,2299161j)>
date_to_date_str(date_str_to_date('31/12/2021'))
#=> "31/12/2021"
Convert array
to an array of hashes
arr =
array.each_with_index.with_object([]) do |(ar,i),a2|
ar.each do |h|
a2 << h.merge(start_date: date_str_to_date(h[:start_date]),
end_date: date_str_to_date(h[:end_date]), idx: i)
end
end
#=> [{:start_date=>#<DateTime: 2021-10-01T00:00:00 00:00 ((2459489j,0s,0n), 0s,2299161j)>,
# :end_date=>#<DateTime: 2021-10-31T00:00:00 00:00 ((2459519j,0s,0n), 0s,2299161j)>,
# :additional_attribute=>5, :idx=>0},
# {:start_date=>#<DateTime: 2021-11-01T00:00:00 00:00 ((2459520j,0s,0n), 0s,2299161j)>,
# :end_date=>#<DateTime: 2021-11-30T00:00:00 00:00 ((2459549j,0s,0n), 0s,2299161j)>,
# :additional_attribute=>10, :idx=>0},
# {:start_date=>#<DateTime: 2021-10-01T00:00:00 00:00 ((2459489j,0s,0n), 0s,2299161j)>,
# :end_date=>#<DateTime: 2021-12-31T00:00:00 00:00 ((2459580j,0s,0n), 0s,2299161j)>,
# :additional_attribute=>20, :idx=>1}]
Note that each of the hashes in a
and b
are mapped to a hash in arr
and those from a
have :idx => 0
and those from b
have :idx => 1
.
Compute the earliest start date and latest end date
start, finish = arr.flat_map { |h| [h[:start_date], h[:end_date]] }.minmax
#=> [#<DateTime: 2021-10-01T00:00:00 00:00 ((2459489j,0s,0n), 0s,2299161j)>,
# #<DateTime: 2021-12-31T00:00:00 00:00 ((2459580j,0s,0n), 0s,2299161j)>]
so
start
#=> #<DateTime: 2021-10-01T00:00:00 00:00 ((2459489j,0s,0n), 0s,2299161j)>
finish
#=> #<DateTime: 2021-12-31T00:00:00 00:00 ((2459580j,0s,0n), 0s,2299161j)>
See Enumerable#flat_map and Array#minmax.
For each date between start
and finish
construct a set of indices of elements of arr
whose ranges include the given date
require 'set'
coverage_by_date = (start..finish).map do |date|
[date,
arr.each_index.select do |i|
(arr[i][:start_date]..arr[i][:end_date]).cover?(date)
end.to_set
]
end
#=> [[#<DateTime: 2021-10-01T00:00:00 00:00 ((2459489j,0s,0n), 0s,2299161j)>, #<Set: {0, 2}>],
# [#<DateTime: 2021-10-02T00:00:00 00:00 ((2459490j,0s,0n), 0s,2299161j)>, #<Set: {0, 2}>],
# ...
# [#<DateTime: 2021-10-31T00:00:00 00:00 ((2459519j,0s,0n), 0s,2299161j)>, #<Set: {0, 2}>],
# [#<DateTime: 2021-11-01T00:00:00 00:00 ((2459520j,0s,0n), 0s,2299161j)>, #<Set: {1, 2}>],
# ...
# [#<DateTime: 2021-11-30T00:00:00 00:00 ((2459549j,0s,0n), 0s,2299161j)>, #<Set: {1, 2}>],
# [#<DateTime: 2021-12-01T00:00:00 00:00 ((2459550j,0s,0n), 0s,2299161j)>, #<Set: {2}>],
# ...
# [#<DateTime: 2021-12-31T00:00:00 00:00 ((2459580j,0s,0n), 0s,2299161j)>, #<Set: {2}>]]
Lastly, remove dates with empty coverage sets and slice resulting array of dates into ranges with equal coverage sets, then map to the desired hashes
coverage_by_date.reject { |_,set| set.empty? }
.slice_when { |(_,set1),(_,set2)| set1 != set2 }
.map do |ar|
attributes = ar.first
.last
.each_with_object(Hash.new(0)) do |i,h|
g = arr[i]
h[g[:idx]] = g[:additional_attribute]
end.values_at(*0..array.size-1)
{ start_date: date_to_date_str(ar.first.first),
end_date: date_to_date_str(ar.last.first),
attributes: attributes }
end
#=> [{:start_date=>"01/10/2021", :end_date=>"31/10/2021", :attributes=>[5, 20]},
# {:start_date=>"01/11/2021", :end_date=>"30/11/2021", :attributes=>[10, 20]},
# {:start_date=>"01/12/2021", :end_date=>"31/12/2021", :attributes=>[0, 20]}]
When (for example)
ar.first.last
#=> #<Set: {0, 2}>
we find that
attributes
#=> [5, 20]
Note:
coverage_by_date.reject { |_,set| set.empty? }
.slice_when { |(_,set1),(_,set2)| set1 != set2 }
.map do |ar|
{ start_date: date_to_date_str(ar.first.first),
end_date: date_to_date_str(ar.last.first),
set: ar.first.last }
end
#=> [{:start_date=>"01/10/2021", :end_date=>"31/10/2021", :set=>#<Set: {0, 2}>},
# {:start_date=>"01/11/2021", :end_date=>"30/11/2021", :set=>#<Set: {1, 2}>},
# {:start_date=>"01/12/2021", :end_date=>"31/12/2021", :set=>#<Set: {2}>}]
See Enumerable#slice_when, the form of Hash::new that takes an argument (the default value) and no block, and Hash#values_at.