1

I have a dates array containing 31 items (shortened here for clarity)

Tuesday 8, November
Wednesday 9, November
#etc.

and a movies hash containing 31 items, each containing movie names and showtimes (shortened here for clarity)

{:movie=>"The Neon Demon", :time=>"4:15 PM"}
{:movie=>"Breaking a Monster", :time=>"6:45 PM"}
{:movie=>"The Citizen", :time=>"9:00 PM"}

I am trying to build a hash for json output which has this structure

[
  {
    "Tuesday 8, November": {
      "movies": [
        {
          "movie": "The Neon Demon",
          "time": "4:15 PM"
        },
        {
          "movie": "Breaking a Monster",
          "time": "6:45 PM"
        },
        {
          "movie": "The Citizen",
          "time": "9:00 PM"
        }
      ]
  },
  {
    "Wednesday 9, November": {
      "movies": [
        {
          "movie": "The First Monday in May",
          "time": "4:15 PM"
        },
        {
          "movie": "The Neon Demon",
          "time": "6:30 PM"
        },
        {
          "movie": "Breaking a Monster",
          "time": "9:00 PM"
        }
      ]
    }, #etc
  }
]

Right now I'm using

Output = []
Output << { dates => { movies: movies }}
puts JSON.pretty_generate Output

But its not arranging elements correctly and I can't figure out the best way to do it. Can anyone give me a tip on how to do it correctly? Thanks

3 Answers 3

4

You're really just one call to zip away from a solution:

movies = [
  {:movie=>"The Neon Demon", :time=>"4:15 PM"},
  {:movie=>"Breaking a Monster", :time=>"6:45 PM"},
  {:movie=>"The Citizen", :time=>"9:00 PM"}
]

dates = [
  'Tuesday 8, November',
  'Wednesday 9, November',
  'Thursday 10, November'
]

dates.zip(movies).to_h
# => {"Tuesday 8, November"=>{:movie=>"The Neon Demon", :time=>"4:15 PM"}, ... }

As a note date formats like that are extremely annoying from a programming perspective since they can't be sorted and are language specific. ISO date format like 2016-11-08 works a lot better in practice.

Sign up to request clarification or add additional context in comments.

4 Comments

To add on to the point about formatting, you can also use l to format the ISO date to look like your locale's time format.
Thanks. zip looks very convenient. I will try it and get back to you.
This is super-simple and certainly works but the output is not as I expected. There should be more than one movie per date, however, I obviously didn't make this clear in my question, so I'm marking this as a solution anyway. Thanks! Thanks also @Eli Sadoff for the point about date formatting.
dates.map { |d| [ d, movies ] }.to_h does it that way, but keep in mind they all share the same movies list. If you need to make individual modifications be sure to .dup the array: movies.dup.
1

Other answers are good. Here is a solution that will give the output you expected.

movies = [
  {:movie=>"The Neon Demon", :time=>"4:15 PM"},
  {:movie=>"Breaking a Monster", :time=>"6:45 PM"},
  {:movie=>"The Citizen", :time=>"9:00 PM"}
]

dates = [
  'Tuesday 8, November',
  'Tuesday 8, November',
  'Thursday 10, November'
]

# Add date to movies array so that we can do group_by in next step
movies_array_with_date = movies.each.with_index{ |m, i| m[:date] = dates[i] }

# Group by the array elements by date
movies_grouped_by_date = movies_array_with_date.group_by {|e| e[:date]}

# Now get rid of date from values of hash as it is not desired in final output
desired_hash = movies_grouped_by_date.each {|k,v| v.each {|a| a.delete(:date)}}

#=> {"Tuesday 8, November"=>
#       [{:movie=>"The Neon Demon", :time=>"4:15 PM"},
#        {:movie=>"Breaking a Monster", :time=>"6:45 PM"}],
#    "Thursday 10, November"=>
#       [{:movie=>"The Citizen", :time=>"9:00 PM"}]}


desired_hash = movies_grouped_by_date.each {|k,v| v.each {|a| a.delete(:date)}}

puts JSON.pretty_generate desired_hash

#=> {
#      "Tuesday 8, November": [
#          {
#            "movie": "The Neon Demon",
#            "time": "4:15 PM"
#          },
#          {
#            "movie": "Breaking a Monster",
#            "time": "6:45 PM"
#          }
#        ],
#        "Thursday 10, November": [
#          {
#            "movie": "The Citizen",
#            "time": "9:00 PM"
#          }
#        ]
#      }

Comments

1

There is rarely just one way to do a calculation in Ruby, and this is no exception. Here's another way. Note that I've allowed for the possibility that two or more movies are showing on the same day.

Code

def aggregate_by_date(dates, movies)
  movies.each_index.with_object({}) { |i,h| (h[dates[i]] ||= []) << movies[i] }
end

Example

movies = [
  {:movie=>"The Neon Demon", :time=>"4:15 PM"},
  {:movie=>"Breaking a Monster", :time=>"6:45 PM"},
  {:movie=>"The Citizen", :time=>"9:00 PM"},
  {:movie=>"Citizen Kane", :time=>"9:40 PM"}
]

dates = [
  'Tuesday 8, November',
  'Wednesday 9, November',
  'Thursday 10, November',
  'Wednesday 9, November'
]

h = aggregate_by_date(dates, movies)
  #=> {"Tuesday 8, November"=>[{:movie=>"The Neon Demon", :time=>"4:15 PM"}],
  #    "Wednesday 9, November"=>[{:movie=>"Breaking a Monster", :time=>"6:45 PM"},
  #                              {:movie=>"Citizen Kane", :time=>"9:40 PM"}],
  #    "Thursday 10, November"=>[{:movie=>"The Citizen", :time=>"9:00 PM"}]}

Explanation

For i = 0 we compute:

(h[dates[i]] ||= []) << movies[i]
  #=> (h[dates[i] = h[dates[i] || []) <<  movies[i]
  #=> (h[dates[i] = nil || []) <<  movies[i]
  #=> (h[dates[i] = []) <<  movies[i]
  #=> h["Tuesday 8, November"]=>[{:movie=>"The Neon Demon", :time=>"4:15 PM"}]

h #=>  {"Tuesday 8, November"=>[{:movie=>"The Neon Demon", :time=>"4:15 PM"}]}

For i = 1 the calculation is similar:

(h[dates[i]] ||= []) << movies[i]
  #=> "Wednesday 9, November"=>[{:movie=>"Breaking a Monster", :time=>"6:45 PM"}] 
h #=> {"Tuesday 8, November"=>[{:movie=>"The Neon Demon", :time=>"4:15 PM"}],
#      "Wednesday 9, November"=>[{:movie=>"Breaking a Monster", :time=>"6:45 PM"}]} 

For i = 2, once more, the calculation is similar:

(h[dates[i]] ||= []) << movies[i]
  #=> "Thursday 10, November"=>[{:movie=>"The Citizen", :time=>"9:00 PM"}]
h #=> {"Tuesday 8, November"=>[{:movie=>"The Neon Demon", :time=>"4:15 PM"}],
  #    "Wednesday 9, November"=>[{:movie=>"Breaking a Monster", :time=>"6:45 PM"}],
  #    "Thursday 10, November"=>[{:movie=>"The Citizen", :time=>"9:00 PM"}]}

But for i = 3, things change, as h now has a key 'Wednesday 9, November', so the calculation is simpler:

(h[dates[i]] ||= []) << movies[i]
  #=> (h[dates[i] = h[dates[i] || []) <<  movies[i]
  #=> (h[dates[i] =
  #     [{:movie=>"Breaking a Monster", :time=>"6:45 PM"}] || []) <<  movies[i]
  #=> h[dates[i] =
        [{:movie=>"Breaking a Monster", :time=>"6:45 PM"}] <<  movies[i]
  #=> h['Wednesday 9, November'] =
  #=>   [{:movie=>"Breaking a Monster", :time=>"6:45 PM"},
  #      {:movie=>"Citizen Kane", :time=>"9:40 PM"}] 

h, which is now returned from the block, equals the hash produced in the example.

Alternative method

Here is another way this could be written, which I present without explanation.

def aggregate_by_date(dates, movies)
  dates.zip(movies).group_by(&:first).
    tap { |h| h.keys.each { |k| h[k] = h[k].map(&:last) } }
end

2 Comments

Thanks for this. Could you explain a little bit what is happening here. I get the gist but, for example, with_object.({}) is totally lost on me!
Using Enumerator#with_object (the object being an initially-empty hash, referenced by the block variable h, which is returned by the block) is just a way of writing the following in one line rather than three: h = {}; movies.each_index { |i| h[dates[i]] = movies[i] }; h. See also Array#each_index. Any other questions?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.