Home > Enterprise >  Rails sync ranges inside of array
Rails sync ranges inside of array

Time:10-19

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"

See DateTime::strptime

​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.

  • Related