在Ruby中,是否可以使用范围作为哈希表的键?

27

我正在尝试创建一个脚本,通过索引查看每一页的页码,并告诉我该条目所在的书籍章节。下面是我正在做的事情的近似描述:

@chapters = {
  1 => "introduction.xhtml",
  2..5 => "chapter1.xhtml",
  6..10 => "chapter2.xhtml",
  11..18 => "chapter3.xhtml",
  19..30 => "chapter4.xhtml" }

def find_chapter(number)
  @chapters.each do |page_range, chapter_name|
    if number === page_range
      puts "<a href=\"" + chapter_name + "\page" + number.to_s + "\">" + number.to_s + </a>"
    end
  end
end

find_chapter(1)将返回我想要的字符串,但find_chapter(15)不会返回任何内容。是否无法像这样使用范围作为键值?


你可以使用任何具有功能性hash方法的东西作为键,由于这在Object中已经定义,因此你几乎必须费尽心思才能找到不能用作键的对象。 - tadman
6个回答

40

您可以使用范围作为哈希键,并且可以使用select非常容易地查找键,如下所示:

@chapters = { 1 => "introduction.xhtml", 2..5 => "chapter1.xhtml", 
              6..10 => "chapter2.xhtml", 11..18 => "chapter3.xhtml",                                         
              19..30 => "chapter4.xhtml" } 

@chapters.select {|chapter| chapter === 5 }
 #=> {2..5=>"chapter1.xhtml"} 
如果您只想要章节名称,只需像这样添加 .values.first
@chapters.select {|chapter| chapter === 9 }.values.first
 #=> "chapter2.xhtml" 

1
我最喜欢这个答案 - 似乎最符合惯用语。+1 - jpalm
22
请尝试运行@chapters.detect{|k,v| k === 5}.last,因为与select不同,detect将在第一次匹配时停止迭代。 - genkilabs

9
这里是一个简洁的方法,只返回第一个匹配键的值:
# setup
i = 17; 
hash = { 1..10 => :a, 11..20 => :b, 21..30 => :c }; 

# find key
hash.find { |k, v| break v if k.cover? i }

8

正如@Sergio Tulentsev所演示的那样,这是可以做到的。然而,通常的方法是使用case when语句。这种方式更加灵活,因为您可以在then子句中执行代码,并且可以使用一个else部分来处理所有未处理的内容。它在内部使用相同的 === 方法。

def find_chapter(number)
  title = case number
    when 1      then "introduction.xhtml"
    when 2..5   then "chapter1.xhtml"
    when 6..10  then "chapter2.xhtml"
    when 11..18 then "chapter3.xhtml"
    when 19..30 then "chapter4.xhtml"
    else "chapter unknown"
  end
  #optionally: do something with title
end

7

当然,只需反转比较即可

if page_range === number

像这样

@chapters = {
  1 => "introduction.xhtml",
  2..5 => "chapter1.xhtml",
  6..10 => "chapter2.xhtml",
  11..18 => "chapter3.xhtml",
  19..30 => "chapter4.xhtml" }

def find_chapter(number)
  @chapters.each do |page_range, chapter_name|
    if page_range === number
      puts chapter_name
    end
  end
end

find_chapter(1)
find_chapter(15)
# >> introduction.xhtml
# >> chapter3.xhtml

它之所以能够这样工作,是因为Range上的===方法具有特殊行为:Range#===。如果您首先放置number,则会调用Fixnum#===,该方法按数字方式比较值。范围不是数字,因此它们不匹配。

从算法的角度来看,这种方法可能比将范围扩展为引用相同值的多个密钥要慢得多。 - tadman
@tadman:很可能。这需要进行测量。当然,这取决于你的范围有多大 :) (扩展可能会消耗大量RAM) - Sergio Tulentsev
大多数人没有数百万本书的章节,但你的观点是需要注意的重要事情。 - tadman
如果您扩大范围,还需要更多的簿记工作,并且出错的可能性更高。 - Michael Mior

4

我在这个论坛上找到了相关话题。他们建议:

class RangedHash
  def initialize(hash)
    @ranges = hash
  end

  def [](key)
    @ranges.each do |range, value|
      return value if range.include?(key)
    end
    nil
  end
end

现在您可以像这样使用它:
ranges = RangedHash.new(
  1..10 => 'low',
  21..30 => 'medium',
  41..50 => 'high'
)
ranges[5]  #=> "low"
ranges[15] #=> nil
ranges[25] #=> "medium"

2

试试这个:

def find_chapter(page_number)
  @chapters.select{ |chapters_key| chapters_key === page_number.to_i}.values.first
end

然后,您只需这样调用它:
find_chapter(10)
=> "chapter2.xhtml"


find_chapter(40)
=> nil

通常,如果答案包括代码意图和解释为什么可以解决问题而不引入其他问题,则更有帮助。 - Tim Diekmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接