如何 "split and group" 基于其属性之一的对象数组

How to "split and group" an array of objects based on one of their properties

上下文和代码示例

我有一个 Array,其中包含一个名为 TimesheetEntry 的 class 实例。

这是 TimesheetEntry 的构造函数:

  def initialize(parameters = {})
    @date       = parameters.fetch(:date)
    @project_id = parameters.fetch(:project_id)
    @article_id = parameters.fetch(:article_id)
    @hours      = parameters.fetch(:hours)
    @comment    = parameters.fetch(:comment)
  end

我使用 .csv 文件中的数据创建了一个 TimesheetEntry 对象数组:

  timesheet_entries = []
  CSV.parse(source_file, csv_parse_options).each do |row|
    timesheet_entries.push(TimesheetEntry.new(
      :date       => Date.parse(row['Date']),
      :project_id => row['Project'].to_i,
      :article_id => row['Article'].to_i,
      :hours      => row['Hours'].gsub(',', '.').to_f,
      :comment    => row['Comment'].to_s.empty? ? "N/A" : row['Comment']
    ))
  end

我还有一个 Set of Hash 包含两个元素,创建如下:

  all_timesheets = Set.new []
  timesheet_entries.each do |entry|
    all_timesheets << { 'date' => entry.date, 'entries' => [] }
  end

现在,我想用 TimesheetEntries 填充该哈希内的数组。 每个哈希数组必须仅包含一个特定日期的 TimesheetEntries。

我是这样做的:

  timesheet_entries.each do |entry|
    all_timesheets.each do |timesheet|
      if entry.date == timesheet['date']
        timesheet['entries'].push entry
      end
    end
  end

虽然这种方法可以完成工作,但效率不高(我对此还很陌生)。

问题

实现相同最终结果的更有效方法是什么?本质上,我想 "split" TimesheetEntry 对象数组,"grouping" 个具有相同日期的对象。

您可以通过将 Set 替换为 Hash 来解决性能问题,这是一个类似字典的数据结构。

这意味着您的内部循环 all_timesheets.each do |timesheet| ... if entry.date ... 将简单地替换为更高效的哈希查找:all_timesheets[entry.date].

此外,无需提前创建密钥,然后然后填充日期组。这些都可以一次性完成:

all_timesheets = {}

timesheet_entries.each do |entry|
  all_timesheets[entry.date] ||= []  # create the key if it's not already there
  all_timesheets[entry.date] << entry
end

散列的一个好处是您可以在遇到不存在的键时自定义它们的行为。您可以使用带块的 constructor 来指定在这种情况下发生的情况。让我们告诉我们的散列自动添加新键并用空数组初始化它们。这允许我们从上面的代码中删除 all_timesheets[entry.date] ||= [] 行:

all_timesheets = Hash.new { |hash, key| hash[key] = [] }

timesheet_entries.each do |entry|
  all_timesheets[entry.date] << entry
end

然而,有一种更简洁的方法可以实现这种分组,使用 Enumerable#group_by method:

all_timesheets = timesheet_entries.group_by { |e| e.date }

当然,还有一种方法可以使它更简洁,使用 another trick:

all_timesheets = timesheet_entries.group_by(&:date)