Harsha M V
Harsha M V

Reputation: 54949

Iterate through Date for the last 7 days

I need to build a array of objects to show data on a graph which shows the data for the last 7 days. At times certain of the 7 days record will be absent in the database and hence i need to show the record and mark the value as 0 to have 7 objects in the array.

Right now i have

[
  {
    "date": "2016-05-14",
    "amount": 6000
  },
  {
    "date": "2016-05-12",
    "amount": 12000
  }
]

What i want is

[
    {
        "date": "2016-05-14",
        "amount": 6000
    },
    {
        "date": "2016-05-13",
        "amount": 0
    },
    {
        "date": "2016-05-12",
        "amount": 12000
    },
    {
        "date": "2016-05-11",
        "amount": 0
    },
    {
        "date": "2016-05-10",
        "amount": 0
    },
    {
        "date": "2016-05-09",
        "amount": 0
    },
    {
        "date": "2016-05-09",
        "amount": 0
    }
]

Upvotes: 2

Views: 2570

Answers (5)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

ts = JSON.parse '[
{
  "date": "2016-05-14",
  "amount": 6000
},
{
  "date": "2016-05-12",
  "amount": 12000
}]' # support ruby < 2.2

((Date.today-6..Date.today).map do |d|
  [d.iso8601, {'amount' => 0, 'date' => d.iso8601 }]
end.to_h.merge ts.group_by { |e| e['date'] }).values.flatten

Here we start with building a hash of requested days mapped to zero values, then merge it against a hash of existing values. The latter takes precedence:

[Date.today].map do |d|
  [d.iso8601, {'amount' => 0, 'date' => d.iso8601 }]
end.to_h
#⇒ { '2016-05-16' => {'amount' => 0, 'date' => '2016-05-16' } }

(in the real hash there are seven items.)

Enumerable#group_by produces the same structure:

[ {'amount' => 42, 'date' => Date.today } ].group_by { |e| e['date'] }
#⇒ { '2016-05-16' => [{'amount' => 42, 'date' => '2016-05-16' }] }

As a last step, we merge the latter into the former and get rid of date keys by taking values of a result and flattening it.


Using Hash#default_proc:

hsh = Hash.new do |h, k|
  ts.detect do |e|
    e['date'] == k.iso8601
  end || { 'amount' => 0, 'date' => k.iso8601 }
end
(Date.today-6..Date.today).map { |d| hsh[d] }

Upvotes: 7

Keith Bennett
Keith Bennett

Reputation: 4970

Here's another approach that I think is fairly clear, and gets the start date from the data rather than Date.today. It assumes that there are no invalid dates in the input, and that the input is not empty.

Note that by specifying "string":... something you are saying the same thing as string:... but in a much more confusing way. I guess this is from JSON data, but it should be parsed into a Ruby array of hashes. I've changed this in my code.

#!/usr/bin/env ruby

require 'date'


def to_date(string)
  Date.strptime(string, '%Y-%m-%d')
end


# Transform the input to a hash whose keys are date objects
# and whose values are the original input records
def transform_input_to_hash(input_records)
  input_records.each_with_object({}) do |record, hsh|
    key = to_date(record[:date])
    hsh[key] = record
  end
end


def fill_in_missing_dates(input_records)
  input_record_hash = transform_input_to_hash(input_records)

  start_date = input_record_hash.keys.first
  output_dates = ((start_date - 6)..start_date).to_a.reverse

  output_dates.each_with_object([]) do |date, array|
    array << if input_record_hash.keys.include?(date)
      input_record_hash[date]
    else
      { date: date, amount: 0 }
    end
  end
end



INPUT_RECORDS = [
  {
    date: "2016-05-14",
    amount: 6000
  },
  {
    date: "2016-05-12",
    amount: 12000
  }
]



output_array = fill_in_missing_dates(INPUT_RECORDS)
puts output_array

By the way, this works in Ruby without Rails. Also, the code is posted at https://gist.github.com/keithrbennett/fd876aac938f1e5d6222896dbd30e8f2.

Upvotes: 1

Michael Gaskill
Michael Gaskill

Reputation: 8042

This will get the right answer:

require 'date'
require 'pp'

data = [
  {
    "date": "2016-05-14",
    "amount": 6000
  },
  {
    "date": "2016-05-12",
    "amount": 12000
  }
]

pp data

today = DateTime.now

7.times do |day|
  current_day = (DateTime.now - day).strftime("%F")
  next if data.find_index {|hash| hash[:date] == current_day }
  data.push({ "date": current_day, "amount": 0 })
end

pp data

This will produce the following output:

[{:date=>"2016-05-14", :amount=>6000},
 {:date=>"2016-05-12", :amount=>12000},
 {:date=>"2016-05-15", :amount=>0},
 {:date=>"2016-05-13", :amount=>0},
 {:date=>"2016-05-11", :amount=>0},
 {:date=>"2016-05-10", :amount=>0},
 {:date=>"2016-05-09", :amount=>0}]

If you want the array sorted by date, you can use this:

data.sort {|a,b| a[:date] <=> b[:date] }.reverse

This will produce this output:

[{:date=>"2016-05-15", :amount=>0},
 {:date=>"2016-05-14", :amount=>6000},
 {:date=>"2016-05-13", :amount=>0},
 {:date=>"2016-05-12", :amount=>12000},
 {:date=>"2016-05-11", :amount=>0},
 {:date=>"2016-05-10", :amount=>0},
 {:date=>"2016-05-09", :amount=>0}]

Upvotes: 1

Anthony E
Anthony E

Reputation: 11235

A functional approach via map reduce is probably simplest and most efficient:

timeseries = [
  {
    "date":   "2016-05-14",
    "amount": 6000
  },
  {
    "date":   "2016-05-12",
    "amount": 12000
  }
]

last_7_days     = 6.days.ago.to_date..Date.today

# Transform array of hashes into a single hash with each key as a Date object.
#  Eg. { "2016-05-12" => 12000, "2016-05-14" => 6000 }
counts_each_day = timeseries.reduce(Hash.new(0)) do |acc, item|
  acc.merge(Date.parse(item[:date]) => item[:amount])
end

# Merge the results with an empty count, if it doesn't exist in our transformed hash.
filled_timeseries = last_7_days.map do |day|
  { date: day.to_s, amount: counts_each_day[day] }
end

Result

[
  { :date => "2016-05-10", :amount => 0 },
  { :date => "2016-05-11", :amount => 0 },
  { :date => "2016-05-12", :amount => 12000 },
  { :date => "2016-05-13", :amount => 0 },
  { :date => "2016-05-14", :amount => 6000 },
  { :date => "2016-05-15", :amount => 0 },
  { :date => "2016-05-16", :amount => 0 }
]

Upvotes: 2

Cheolho Jeon
Cheolho Jeon

Reputation: 471

I hope this helps.

data = [
{
  "date": "2016-05-14",
  "amount": 6000
},
{
  "date": "2016-05-12",
  "amount": 12000
}] # Given data
dates = data.map do |datum| datum[:date] end # Extract dates from data.
today = Date.today # Get today.
dates_list = 7.times.map do |index| (today + index).strftime("%F") end # Get 7days from today.
dates_list.each do |date|
  next if dates.include? date # if data already includes the date, such as "2016-05-14", pass.
  data << {"date": date, "amount": 0} # if data does not include the date, append it with amount 0.
end

Upvotes: 1

Related Questions