Ruby Koans的triangle.rb问题的更加优雅的解决方案

17

我一直在学习Ruby Koans,现在到了about_triangle_project.rb这个部分,你需要编写一个名为triangle的方法。

这些项目的代码可以在这里找到:

https://github.com/edgecase/ruby_koans/blob/master/koans/about_triangle_project.rb

https://github.com/edgecase/ruby_koans/blob/master/koans/triangle.rb

在triangle.rb中,我创建了以下方法:
def triangle(a, b, c)
  if ((a == b) && (a == c) && (b == c))
    return :equilateral
  elsif ((a == b) || (a == c) || (b == c))
    return :isosceles
  else
    return :scalene
  end
end

我知道从Chris Pine的"Learn to Program"中阅读到,做事情总有多种方法。虽然上面的代码可以工作,但我不禁想到是否有更优雅的方法来实现这一点。是否有人愿意提供他们的想法,如何使这种方法更有效和紧凑?
另一件让我好奇的事情是,为什么在确定等边三角形时,我无法创建条件(a == b == c)。这是等边三角形的证明,但Ruby不支持这种语法。是否有一个简单的解释?

1
== 是一个操作符,它接受两个值(就像 */ 一样)。它返回 truefalse。为了避免混淆,这种写法是不合法的(例如 1 == 1 == 1 将被计算为 false,因为它等同于 (1 == 1) == 1)。 - glebm
2
你可以使用“等边三角形”的传递性质来节省一些代码:(a == b) && (b == c)。 - pkananen
Python支持“a == b == c”语法(甚至是“a < b <= c”),但在编程语言中,这是一个例外而不是规则。 - Todd Owen
9个回答

59

这很容易解释:

== 在 Ruby 中是一个操作符,它执行特定的函数。操作符有确定它们被应用的顺序的规则 - 例如,a + 2 == 3 会先计算加法再检查相等性。但是只有一个操作符会被计算一次。把两个相等性检查放在一起毫无意义,因为相等性检查会返回 truefalse。一些语言允许这样做,但仍然不能正确工作,因为如果 ab 相等,那么你将评估出 true == c,即使在数学术语中 a == b == c,这显然是不正确的。

至于更优雅的解决方案:

case [a,b,c].uniq.size
when 1 then :equilateral
when 2 then :isosceles
else        :scalene
end
或者,更加简洁(但可读性较差):
[:equilateral, :isosceles, :scalene].fetch([a,b,c].uniq.size - 1)

谢谢你,Chuck!我还没在 koans 中使用 case、uniq 或 fetch,但那非常酷(这就是为什么我喜欢 Ruby!)。 - erinbrown
3
+1 表示对 uniq.size 给予赞同,这很优雅。有趣的是你选择使用了 fetch,因为 [...][[...].uniq.size] 也是有效的。 - Phrogz
@Phrogz:一开始我就是那样写的,但它实在是难以阅读,就像人们总是嘲笑的 Perl 代码那样,所以我想 fetch 至少近似于我想要阅读的东西。 - Chuck
2
我想出了这段代码:[nil, :等边, :等腰, :不等边][[a,b,c].uniq.size],但我认为你的代码更易读。 - Pablo B.
很棒的东西,但请注意简洁解决方案中的两个错别字(等腰和不等边)。修改的更改太少了。 - Jamie Schembri
@Jamie Schembri:已修复。谢谢你的提醒。我很喜欢自己把“等边”的拼写写对了,但却没能把更短的“不等边”写对。 - Chuck

12

另一种方法:

def triangle(a, b, c)
  a, b, c = [a, b, c].sort
  raise TriangleError if a <= 0 or a + b <= c
  return :equilateral if a == c
  return :isosceles if a == b or b == c
  return :scalene
end

巧妙地利用排序来避免不必要的比较。 - Todd Owen
我必须在结尾处添加以下代码才能使其正常工作:class TriangleError < StandardError end - SteveO7

6

我借鉴了Chuck的uniq.size技术,并将其融入到一个面向对象的解决方案中。最初,我只想将参数验证作为守卫子句提取出来以保持单一职责原则,但由于两种方法都在操作相同的数据,所以我认为它们应该放在一个对象中。

# for compatibility with the tests
def triangle(a, b, c)
  t = Triangle.new(a, b, c)
  return t.type
end

class Triangle
  def initialize(a, b, c)
    @sides = [a, b, c].sort
    guard_against_invalid_lengths
  end

  def type
    case @sides.uniq.size
    when 1 then :equilateral
    when 2 then :isosceles
    else :scalene
    end
  end

  private
  def guard_against_invalid_lengths
    if @sides.any? { |x| x <= 0 }
      raise TriangleError, "Sides must be greater than 0"
    end

    if @sides[0] + @sides[1] <= @sides[2]
      raise TriangleError, "Not valid triangle lengths"    
    end
  end
end

这是关于Triangle项目第二部分Tests的绝佳答案。 我复制/粘贴了您的解决方案,它比我的机器代码版本更优雅。 - TALLBOY

6
def triangle(a, b, c)
  if a == b && a == c                # transitivity => only 2 checks are necessary
    :equilateral
  elsif a == b || a == c || b == c   # == operator has the highest priority
    :isosceles
  else
    :scalene                         # no need for return keyword
  end
end

谢谢你,glebm!(还有上面的回答也谢谢。) - erinbrown

5
class TriangleError < StandardError
end

def triangle(a, b, c)
  sides = [a,b,c].sort

  raise TriangleError if sides.first <= 0 || sides[2] >= sides[1] + sides[0]
  return :equilateral if sides.uniq.length  == 1
  return :isosceles if sides.uniq.length  == 2
  :scalene
end

3

这是我的解决方案:

def triangle(a, b, c)
  sides = [a, b, c].sort
  raise TriangleError, "Invalid side #{sides[0]}" unless sides[0] > 0
  raise TriangleError, "Impossible triangle" if sides[0] + sides[1] <= sides[2]
  return [:scalene, :isosceles, :equilateral][ 3 - sides.uniq.size ]
end

我喜欢它充分利用排序来提供更精确的错误反馈,而且是最短和最易读的。不需要返回值。 - dansalmo

2

哦,我不知道uniq - 所以从很久以前的Smalltalk(ages ago)开始,我使用了:

require 'set'
def triangle(a, b, c)
  case [a, b, c].to_set.count
    when 1 then :equilateral
    when 2 then :isosceles
    else :scalene
  end
end

我寻找了一个比我实现的更优雅的解决方案,并在一年半后发现了这篇文章... 但我必须说我真的很喜欢这个解决方案。它是一种漂亮干净的方法,也非常易读;赞! - bigtunacan

2

作为一个来自Matlab世界的人,我习惯于使用数组函数'any'和'all',很高兴在Ruby中也找到了它们。所以:

def triangle(a, b, c)
  eqs = [a==b, a==c, b==c]
  eqs.all?? :equilateral : eqs.any?? :isosceles : :scalene
end

我不确定这是否最佳,无论是在可读性还是计算时间上...(ruby新手)。

1
这是我的解决方案:
def triangle(a, b, c)
  return :equilateral if a == b and b == c
  return :isosceles if ( a == b or b == c or a == c )
  return :scalene
end

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