Sig
Sig

Reputation: 5916

Union two arrays while filling the gaps

I have a day object. It has two attributes: name and magnitude where name is the name of the day ("Monday", "Tuesday" ....) while magnitude is an integer.

I need to build a week which must always return an array of 7 days, one for each day of the week.

However, sometimes, the array I get has less than 7 days. I.e.

days = [<Day name: "Monday", magnitude: 4>, 
        <Day name: "Friday", magnitude: 3>, 
        <Day name: "Sunday", magnitude: 8>]

So, to build the week I came up with the following code

default = Date::DAYNAMES.rotate(1).map{ |day| Day.new(name: day, magnitude: 0) }

default.map do |day|
 days.detect do |other|
   other.day == day.day
 end || day
end

This works but I find it pretty ugly. I'd rather perform a union between the two arrays. However, I must preserve the order of the days in the array. Monday must always be the first day while Sunday the last one.

Any ideas on how to improve the code above?

Upvotes: 1

Views: 65

Answers (3)

3limin4t0r
3limin4t0r

Reputation: 21110

I would go for a similar answer as already given by Cary Swoveland. However I'd use the following:

day_lookup = days.to_h { |day| [day.name, day] }
day_lookup.default_proc = ->(_, name) { Day.new(name: name, magnitude: 0) }
days = day_lookup.values_at(*Date::DAYNAMES.rotate(1))

So, what did I change? If you use Ruby 2.6.0 or greater you can use Array#to_h with a block, which makes creating a hash from array a lot cleaner in my opinion.

Secondly I only return a default value if a name is not present, but don't set the value in the day_lookup hash. If you need to access a day multiple times this might be a good idea to do, however in this scenario we only call values_at once and then we're done with the hash.

Upvotes: 0

iGian
iGian

Reputation: 11183

You can consider to map against an array of weekdays:

weekdays = %w(Monday Tuesday Wednesday Thursday Friday Saturday Sunday)

Where you can use the constant Date::DAYNAMES available requiring the date library.

Then, given your array (I'm not using the class Day) for example as

days = [ ['Monday', 4], ['Friday', 3], ['Sunday', 8] ]

you can do:

weekdays.map { |weekday| days.find { |e| e.first == weekday } || [weekday, 0] }
#=> [["Monday", 4], ["Tuesday", 0], ["Wednesday", 0], ["Thursday", 0], ["Friday", 3], ["Saturday", 0], ["Sunday", 8]]

Or, using your class:

weekdays.map { |weekday| days.find { |e| e.name == weekday } || Day.new(weekday, 0) }

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110675

Data

class Day
  attr_accessor :name, :magnitude
  def initialize(name, magnitude)
    @name = name
    @magnitude = magnitude
  end
end

days = [["Monday", 4], ["Friday", 3], ["Sunday", 8]].map {|args|
  Day.new(*args) }
  #=> [#<Day:0x00005a1be9157548 @name="Monday", @magnitude=4>,
  #    #<Day:0x00005a1be91574d0 @name="Friday", @magnitude=3>,
  #    #<Day:0x00005a1be91574a8 @name="Sunday", @magnitude=8>]

Code

require 'date'

h = days.each_with_object({}) { |inst,h| h[inst.name] = inst }
  #=> {"Monday"=>#<Day:0x00005a1be9157548 @name="Monday", @magnitude=4>,
  #    "Friday"=>#<Day:0x00005a1be91574d0 @name="Friday", @magnitude=3>,
  #    "Sunday"=>#<Day:0x00005a1be91574a8 @name="Sunday", @magnitude=8>} 

h.default_proc = ->(h,d) { h[d] = Day.new(d,0) }
  #=> #<Proc:0x00005a1be93ce858@(irb):26 (lambda)> 

h.values_at(*Date::DAYNAMES.rotate(1))
  #=> [#<Day:0x00005a1be9157548 @name="Monday", @magnitude=4>,
  #    #<Day:0x00005a1be93d8ec0 @name="Tuesday", @magnitude=0>,
  #    #<Day:0x00005a1be93d8e98 @name="Wednesday", @magnitude=0>,
  #    #<Day:0x00005a1be93d8e70 @name="Thursday", @magnitude=0>,
  #    #<Day:0x00005a1be91574d0 @name="Friday", @magnitude=3>,
  #    #<Day:0x00005a1be93d8e48 @name="Saturday", @magnitude=0>,
  #    #<Day:0x00005a1be91574a8 @name="Sunday", @magnitude=8>] 

Explanation

See Hash#default_proc= and Hash#values_at. If h does not have a key day the default proc causes h to return a new Day object with @name equal to the value of day and @magnitude equal to zero.

The first two expressions could be combined using (for example) Object#tap:

h = days.each_with_object({}) { |inst,h| h[inst.name] = inst }.
         tap { |h| h.default_proc = ->(h,d) { h[d] = Day.new(d,0) }

Upvotes: 3

Related Questions