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?
2 Answers 2
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 ofstrftime
.- You can call
Range#map
directly; you don't need to doto_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) }
-
\$\begingroup\$ The example has just two dates and two values, but the code should work for multiple data points. \$\endgroup\$200_success– 200_success2016年11月16日 19:47:52 +00:00Commented Nov 16, 2016 at 19:47
-
\$\begingroup\$ Edited. Well, the result, unsurprisingly, is not very different than your answer, not sure if it makes sense to keep it. \$\endgroup\$tokland– tokland2016年11月16日 19:57:49 +00:00Commented Nov 16, 2016 at 19:57
-
1\$\begingroup\$ 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
. \$\endgroup\$200_success– 200_success2016年11月16日 20:00:32 +00:00Commented Nov 16, 2016 at 20:00