3
\$\begingroup\$

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?

\$\endgroup\$
3
\$\begingroup\$

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)
| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

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) }
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ The example has just two dates and two values, but the code should work for multiple data points. \$\endgroup\$ – 200_success Nov 16 '16 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 Nov 16 '16 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 Nov 16 '16 at 20:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.