【问题标题】:Can't iterate over Time objects in Ruby无法在 Ruby 中遍历 Time 对象
【发布时间】:2014-09-03 17:22:27
【问题描述】:

我正在编写一个约会表格,让用户选择一个日期。然后它将获取日期并对照 Google 日历检查在上午 10:00 到下午 5:00 的 30 分钟时间间隔范围内该日期的可用时间段。

在我的日历类中,我有一个 available_times 方法:

def available_times(appointment_date)
    appointment_date_events = calendar.events.select { |event| Date.parse(event.start_time) == appointment_date } 

    conflicts = appointment_date_events.map { |event| [Time.parse(event.start_time), Time.parse(event.end_time)] }
    results = resolve_time_conflicts(conflicts) 
end

此方法需要一个日期,并为该日期的每个事件获取 start_timeend_time。然后它调用resolve_time_conflicts(conflicts):

def resolve_time_conflicts(conflicts)
    start_time = Time.parse('10:00am') 
    available_times = [] 
    14.times do |interval_multiple|
      appointment_time = (start_time + interval_multiple * (30 * 60))  
      available_times << appointment_time unless conflicts.each{ |conflict| (conflict[0]..conflict[1]).include?(appointment_time)}  
    end
      available_times 
end

当我尝试迭代冲突数组时,会引发“无法迭代时间”错误。我试图在冲突数组上调用to_enum,但仍然遇到同样的错误。

我在 SO 上看到的所有其他问题都引用了 step 方法,这似乎不适用于这种情况。

更新:

Thanks @caryswoveland and @fivedigit. I combined both of your answers, which were very helpful for different aspects of my solution:

  def available_times(appointment_date)
    appointment_date_events = calendar.events.select { |event| Date.parse(event.start_time) == appointment_date } 

    conflicts = appointment_date_events.map { |event| DateTime.parse(event.start_time)..DateTime.parse(event.end_time) }
    results = resolve_time_conflicts(conflicts) 
  end

  def resolve_time_conflicts(conflicts)
    date = conflicts.first.first   
    start_time = DateTime.new(date.year, date.month, date.day, 10, 00).change(offset: date.zone) 
    available_times = [] 
    14.times do |interval_multiple|
      appointment_time = (start_time + ((interval_multiple * 30).minutes))
      available_times << appointment_time unless conflicts.any? { |conflict| conflict.cover?(appointment_time)}  
    end
      available_times 
  end

【问题讨论】:

  • 如果您将conflicts 计算为范围数组(Time.parse(event.start_time)..Time.parse(event.end_time))(而不是数组数组),则可以编写...|conflict| conflict.cover?(...
  • 你能举一个calendar.events元素的例子吗?
  • @CarySwoveland - 谢谢,这更有意义!看了我上面的修改后,你还想看 calender.events 的元素吗?
  • 仅当您仍然需要知道如何使 appointment_time 日期与 conflicts 日期相同时。

标签: ruby time google-calendar-api


【解决方案1】:

问题出在这个位:

(conflict[0]..conflict[1]).include?(appointment_time)
# TypeError: can't iterate from Time

您正在创建时间范围,然后检查 appointment_time 是否在该范围内。这就是导致您遇到的错误的原因。

您应该使用cover?,而不是include?

(conflict[0]..conflict[1]).cover?(appointment_time)

这假定conflict[0] 是最早的时间。

【讨论】:

  • 谢谢@fivedigity,我用过封面?而不是包括?,虽然我不确定确切的区别是什么:)
  • Time 实例的情况下,include? 尝试迭代范围,这反过来要求范围中的对象实现succ 方法,该方法返回对象的后继对象。对于Time,此方法已过时,因此会抛出TypeErrorcover? 使用begin &lt;= value &lt;= end 检查值是否在范围内,这是比较而不是迭代。因此,为什么要求最早的时间是范围的开始。
  • V,对于我之前发表的不正确评论,我已将其删除,我深表歉意。
  • 实际上,您发布的示例演示了TypeError 的实际根本原因;)
【解决方案2】:

例外

@fivedigit 解释了引发异常的原因。

其他问题

你需要any?,而你有each

appointment_times = []
  #=> []
appointment = 4
  #=> 4
conflicts = [(1..3), (5..7)]
  #=> [1..3, 5..7]

appointment_times << 5 unless conflicts.each { |r| r.cover?(appointment) }
  #=> nil
appointment_times
  #=> []

appointment_times << 5 unless conflicts.any? { |r| r.include?(appointment) }
  #=> [5]
appointment_times
  #=> [5]

我建议您将appointment_time 转换为Time 对象,制作conflicts 和元素数组[start_time, end_time],然后将appointment_time 与端点进行比较:

...unless conflicts.any?{ |start_time, end_time|
     start_time <= appointment_time && appointment_time <= end_time }  

旁白:Range#include? 仅在端点为“数字”时查看端点(如 Range#cover? does)。 Range#include? 只需要在端点是 Time 对象时查看端点,但我不知道 Ruby 是否将 Time 对象视为“数字”。我想可以看看源代码。有人知道吗?

替代方法

我想建议一种不同的方式来实现您的方法。我会举个例子。

假设约会以 15 分钟为单位,第一个时间段为上午 10:00 至上午 10:15,最后一个时间段为下午 4:45 至下午 5:00。 (当然,块可以更短,持续时间短至 1 秒。)

让 10:00am-10:15am 成为 block 0,10:15am-10:30am 成为 block 1,依此类推,直到 block 27,4:45pm-5:00pm。

接下来,将conflicts 表示为由[start, end] 给出的块范围数组。假设有约会:

10:45am-11:30am (blocks 3, 4 and 5)
 1:00pm- 1:30pm (blocks 12 and 13)
 2:15pm- 3:30pm (blocks 17, 18 and 19)

然后:

conflicts = [[3,5], [12,13], [17,19]]

您必须编写一个返回conflicts 的方法reserved_blocks(appointment_date)

剩下的代码如下:

BLOCKS = 28
MINUTES = ["00", "15", "30", "45"]
BLOCK_TO_TIME = (BLOCKS-1).times.map { |i|
  "#{i<12 ? 10+i/4 : (i-8)/4}:#{MINUTES[i%4]}#{i<8 ? 'am' : 'pm'}" }
  #=> ["10:00am", "10:15am", "10:30am", "10:45am",
  #    "11:00am", "11:15am", "11:30am", "11:45am",
  #    "12:00pm", "12:15pm", "12:30pm", "12:45pm",
  #     "1:00pm",  "1:15pm",  "1:30pm",  "1:45pm",
  #     "2:00pm",  "2:15pm",  "2:30pm",  "2:45pm",
  #     "3:00pm",  "3:15pm",  "3:30pm",  "3:45pm",
  #     "4:00pm",  "4:15pm",  "4:30pm",  "4:45pm"]

def available_times(appointment_date)
  available = [*(0..BLOCKS-1)]-reserved_blocks(appointment_date)
                .flat_map { |s,e| (s..e).to_a }
  last = -2 # any value will do, can even remove statement
  test = false
  available.chunk { |b| (test=!test) if b > last+1; last = b; test }
           .map { |_,a| [BLOCK_TO_TIME[a.first], 
             (a.last < BLOCKS-1) ? BLOCK_TO_TIME[a.last+1] : "5:00pm"] }
end

def reserved_blocks(date) # stub for demonstration.
  [[3,5], [12,13], [17,19]]
end

让我们看看我们得到了什么:

available_times("anything") 
  #=> [["10:00am", "10:45am"],
  #    ["11:30am",  "1:00pm"],
  #    [ "1:45pm",  "2:15pm"], 
  #    [ "3:00pm",  "5:00pm"]]

说明

这是发生了什么:

appointment_date = "anything" # dummy for demonstration

all_blocks = [*(0..BLOCKS-1)]
  #=> [ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13,
  #    14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
reserved_ranges = reserved_blocks(appointment_date)
  #=> [[3, 5], [12, 13], [17, 19]]
reserved = reserved_ranges.flat_map { |s,e| (s..e).to_a }
  #=> [3, 4, 5, 12, 13, 17, 18, 19]
available = ALL_BLOCKS - reserved
  #=> [0, 1, 2, 6, 7, 8, 9, 10, 11, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27]

last = -2
test = false
enum1 = available.chunk { |b| (test=!test) if b > last+1; last = b; test }
  #=> #<Enumerator: #<Enumerator::Generator:0x00000103063570>:each>

我们可以将它转换为一个数组,看看如果map 没有跟随它会传递到块中的值:

enum1.to_a
  #=> [[true, [0, 1, 2]],
  #    [false, [6, 7, 8, 9, 10, 11]],
  #    [true, [14, 15, 16]],
  #    [false, [20, 21, 22, 23, 24, 25, 26, 27]]]

Enumerable#chunk 将枚举数的连续值分组。它通过对test 的值进行分组,并在遇到非连续值时在truefalse 之间翻转其值。

enum2 = enum1.map
  #=> #<Enumerator: #<Enumerator: (cont.)
      #<Enumerator::Generator:0x00000103063570>:each>:map>

enum2.to_a
  #=> [[true, [0, 1, 2]],
  #    [false, [6, 7, 8, 9, 10, 11]],
  #    [true, [14, 15, 16]],
  #    [false, [20, 21, 22, 23, 24, 25, 26, 27]]]

您可能会将enum2 视为“复合”枚举器。

最后,我们将传递到块中的enum2 的每个值的第二个元素(块变量a,对于传递的第一个元素等于[0,1,2])转换为表示为12-的范围小时时间。 enum2truefalse)的每个值的第一个元素没有被使用,所以我用下划线替换了它的块变量。这提供了所需的结果:

enum2.each { |_,a|[BLOCK_TO_TIME[a.first], \
        (a.last < BLOCKS-1) ? BLOCK_TO_TIME[a.last+1] : "5:00pm"] }
  #=> [["10:00am", "10:45am"],
  #    ["11:30am",  "1:00pm"],
  #    [ "1:45pm",  "2:15pm"], 
  #    [ "3:00pm",  "5:00pm"]]

【讨论】:

  • 感谢您的澄清。实际上,约会时间是一个时间对象。 [3] pry(#)> 约会时间.class=> 时间我遇到的问题是如何让约会时间在约会日期 - 冲突的同一日期。
  • 啊,我错过了 appointment_time = (start_time +... 中的 start_time 作为 Time 对象,这使得 appointment_time 也成为 Time 对象。我会看看你提出的日期问题。
  • Range#include? 仅在少数情况下检查 beginend 值以确定值是否在范围内。它对Fixnums 和长度为1 的字符串执行此操作。对于其他类型,它使用Range#each 在范围上进行迭代,这将检查开始对象是否响应succ。这仅适用于离散值,Time 不是。这也意味着在这种情况下,Ruby 不会将Time 视为数字。
  • 感谢@fivedigit 的解释。注意Range#include? 检查端点的Numeric 类型,而不仅仅是Fixnums(2.1..3.4).include?(2.8) #=&gt; true,当然succ 没有为Float 定义。我也认为 1-character strings 是另一种豁免,但我在文档中没有看到。我想知道为什么include? 不只是使用端点,比如cover?,直到我想出了一个case,这两种方法在('aa'..'zz') 范围内返回不同的值。
  • @CarySwoveland - 很抱歉回复晚了,因为我刚刚看到您更新的替代解决方案。感谢您的解决方案,这是一种非常简洁的方法!
【解决方案3】:

将您的范围从时间范围转换为整数范围:

range = (conflict[0].to_i..conflict[1].to_i)

然后像使用 include? 一样使用 === 运算符:

conflict === appointment_time

编辑:显然,您也可以将appointment_time 转换为整数并仍然使用include?,因为该范围现在只是一个整数范围。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-28
    • 1970-01-01
    • 1970-01-01
    • 2017-01-11
    相关资源
    最近更新 更多