# Ensure continuous date range

I have two arrays

dates = ["20161010","20161013"]
values = [5,3]


which represent dates and a view count. To display them in a graph, I also need the values between the dates, which are 0:

["20161010","20161011","20161012","20161013"]
[5,0,0,3]


I came up with a fairly complicated solution:

dates = ["20161010","20161013"]
values = [5,3]
first_date = Date.new(Integer(dates.first[0..3]), Integer(dates.first[4..5]), Integer(dates.first[6..7]))
last_date = Date.new(Integer(dates.last[0..3]), Integer(dates.last[4..5]), Integer(dates.last[6..7]))

# create a continous range
range = (first_date..last_date).to_a.map {|d| d.strftime("%Y%m%d")}
# store the indexes of the original dates inside the new range
date_idxs = dates.map {|d| range.index(d) }

# create a zero filled array with length of our range
filled_values = Array.new(range.length, 0)

values.each_with_index do |value,idx|
idx_in_filled_values = date_idxs[idx]
filled_values[idx_in_filled_values] = value
end

puts range # ["20161010", "20161011", "20161012", "20161013"]
puts filled_values # [5, 0, 0, 3]


I think thats not a good solution. Any ideas on how to improve the code? Or a completely different approach?

Several aspects of your code could use improvement:

• It would be nice if the code were packaged in a function.
• Date.new(Integer(dates.first[0..3]), Integer(dates.first[4..5]), Integer(dates.first[6..7])) is rather verbose and ugly.

I recommend DateTime.strptime for date parsing — it's the inverse of strftime.

• You can call Range#map directly; you don't need to do to_a first.
• dates.map { |d| range.index(d) } could be problematic for performance, since it is O(n2).

Instead, you should make a Hash.

Here is one solution:

require 'date_core'

DATE_FMT = '%Y%m%d'

def filled_date_series(dates, values, default_value=0)
data = Hash[dates.zip(values)]
datetime_range = (DateTime.strptime(dates.first, DATE_FMT) ..
DateTime.strptime(dates.last,  DATE_FMT))
date_range = datetime_range.map { |dt| dt.strftime(DATE_FMT) }
[
date_range,
date_range.map { |d| data[d] || default_value }
]
end

dates = ["20161010", "20161013"]
values = [5, 3]
range, filled_values = filled_date_series(dates, values)


Some notes:

• dates.first[0..3]. Use Date.parse or Date.strptime.

• Array.new(range.length, 0). As a general rule, don't use that imperative style to create arrays. Use enumerable methods instead to build expressions.

• Is your code inside a class or module or at least a function?

I'd write it in functional style:

require 'date'

string_dates = ["20161010", "20161013"]
values = [5, 3]

start_date = Date.parse(string_dates.first)
end_date = Date.parse(string_dates.last)
values_by_date = string_dates.zip(values).to_h
range = (start_date..end_date).map { |date| date.strftime("%Y%m%d") }
filled_values = range.map { |date| values_by_date.fetch(date, 0) }

• The example has just two dates and two values, but the code should work for multiple data points. – 200_success Nov 16 '16 at 19:47
• Edited. Well, the result, unsurprisingly, is not very different than your answer, not sure if it makes sense to keep it. – tokland Nov 16 '16 at 19:57
• The general idea is the same, but there are some differences in the details that are worth showing, such as Date.parse, .to_h, and .fetch. – 200_success Nov 16 '16 at 20:00