Reputation: 54949
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
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
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
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
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
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