Ruby求标准输入整数之和

3

我有:

$ ruby -v
ruby 2.0.0p648 (2015-12-16 revision 53162) [universal.x86_64-darwin16]

假设您有一个整数序列1..n,对于初学 Ruby 的人来说,他们会这样求和:
$ ruby -e 's=0
     for i in 1..500000
        s+=i
     end
     puts s'
125000250000

现在假设我有来自 stdin 的相同序列:

$ seq 1 500000 | ruby -lne 'BEGIN{s=0}
                            s+=$_.to_i
                            END{puts s} '   
125000250000

到目前为止一切都好。现在将终值从500,000增加到5,000,000:
$ ruby -e 's=0
         for i in 1..5000000
            s+=i
         end
         puts s'
12500002500000   <=== CORRECT

$ seq 1 5000000 | ruby -lne 'BEGIN{s=0}
                             s+=$_.to_i
                             END{puts s} '
500009500025     <=== WRONG!

它会产生不同的总和。

awkperl 都使用相同的序列产生了正确的结果:

$ seq 1 5000000 | awk '{s+=$1} END{print s}'
12500002500000
$ seq 1 5000000 | perl -nle '$s+=$_; END{print $s}'
12500002500000

为什么 Ruby 的求和结果不正确?我认为这不是溢出问题,因为相同的输入在 awk 和 perl 中能正常工作。


结论:

感谢 David Aldridge 进行诊断。

  1. OS X and BSD seq converts to a float output at 1,000,000 while GNU seq supports arbitrary precision integers. OS X seq is useless as a source of integers greater than 1,000,000. Example on OS X:

    $ seq  999999 1000002
    999999
    1e+06
    1e+06
    1e+06
    
  2. The ruby method .to_i silently converts a partial string to an integer and that was the 'bug' in this case. Example:

    irb(main):002:0> '5e+06'.to_i
    #=> 5
    
  3. The 'correct' line in the script is to either use $_.to_f.to_i to use floats or to use Integer($_) to not have the script fail silently. awk and perl parse 5e+06 into a float, and ruby does not implicitly:

    $ echo '5e+06' | awk '{print $1+0}'
    5000000
    $ echo '5e+06' | ruby -lne 'print $_.to_i+0'
    5
    
  4. And thanks to Stefan Schüßler for opening a Ruby feature request regarding .to_i behavior.


1
FYI,我已经为这个问题开了一个功能请求 - Stefan
2
值得一提的是,Ruby 2.4中的Enumerable#sum方法已经针对此进行了优化,因此(1..5000000).sum几乎可以立即返回结果。 - steenslag
我可以问一下为什么要点踩吗?这样我就可以纠正这个问题或者以后的问题了。对于其他的 Ruby 初学者来说,这里有什么不清楚或者没有用的地方吗? - dawg
2个回答

5

我不确定这是否是100%的答案,但我注意到:


可能有其他解决方案,但以下方法可以尝试:


seq 500000 500001 | ruby -lne 'BEGIN{}
                             puts $_
                             END{} '
500000
500001

... but ...

seq 5000000 5000001 | ruby -lne 'BEGIN{}
                             puts $_
                             END{} '
5e+06
5e+06

因此,#to_i方法对于将值转换为整数的“宽容”(relaxed)方法仍然有效。

seq 5000000 5000001 | ruby -lne 'BEGIN{}
                             puts $_.to_i
                             END{} '
5
5

...但更严格的#to_int不会这样做

seq 5000000 5000001 | ruby -lne 'BEGIN{}
                             puts $_.to_int
                             END{} '
-e:2:in `<main>': undefined method `to_int' for "5e+06":String (NoMethodError)

编辑:我还注意到:

seq 5000000 5000001

5e+06
5e+06

因此,必须向seq传递-f标志以获取整数格式。

再次编辑:

最终答案:

seq -f %f 1 5000000 | ruby -lne 'BEGIN{s=0}
                                  s+=$_.to_i
                                 END{puts s} '

12500002500000

你也可以使用 seq 1 5000000 | tail :) - Sergio Tulentsev
为了克服这个问题,可以使用s+=$_.to_f.to_i - Sergio Tulentsev
我希望我能弄清楚为什么 $_ 会将最后一行读取为指数格式的字符串转换。这就像 Excel 会做的事情一样。 - David Aldridge
测试FreeBSD可能会很有趣,我相信在MacOS中有一个FreeBSD版本的seq,由于某种未知原因它会表现出这种方式。另外,您介意尝试alias | grep seq吗? - Aleksei Matiushkin
to_int doesn't work, because that methods doesn't exist (hence the NoMethodError). The correct method is Integer($_), which results in ArgumentError: invalid value for Integer(): "5e+06" - Stefan
显示剩余4条评论

1
为了解释 e-notation 的输出,OS X man 页面 seq 提供了一些见解:

使用 printf(3) 样式的格式来打印每个数字。[...] 默认值是 %g

因此,seq 的输出等同于 Ruby 的:
sprintf('%g', 100000)
#=> "100000"

sprintf('%g', 1000000)
#=> "1e+06"

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