ruby:将一个特殊数组评估为任意长的三元 if...elsif...else 语句(或三元表达式)
ruby: evaluate a special array as an arbitrarily long ternary if...elsif...else statement (or ternary expression)
我将一些决策数据存储在数组中,如下所示:[condition, result, ..., condition, result, default]
,所以基本上是三元表达式 (c ? r : ... c ? r : d
),我正在用这段代码对它们进行评估:
class Array
def ternary &block
# the block checks if a condition is true
i = 0
while true
if i % 2 == 1 or i == length - 1
return self[i]
else
i += yield(self[i]) ? 1 : 2
end
end
end
end
['false', 0, 'true', 1, 'true', 2, 3].ternary {|i| i == 'true'}
# => 1
[1, 2, 3, 4, 5].ternary {|i| i > 6}
# => 5 (defaults to last value because all conditions failed)
我想知道是否有更快的内置方法或如何改进此代码。
注意:有任意多个条件,[only_possible_answer]
也应该有效
编辑:到目前为止的答案(测试了同一数组的 1 000 000 次迭代):
设置
flat = ['false', 0, 'false', 1, 'false', 2, 'false', 3, 'false', 4, 'true', 5, 6]
nested = [['false', 0], ['false', 1], ['false', 2], ['false', 3], ['false', 4], ['true', 5], [6]]
Option = Struct.new :condition, :result
Default = Struct.new :result
class Default
def call; self; end
# otherwise:
# undefined method ‘call’ for #<struct Default result=whatever>
end
options = [Option.new('false', 0), Option.new('false', 1), Option.new('false', 2), Option.new('false', 3), Option.new('false', 4), Option.new('true', 5)]
class Array
def ternary_flat_slow
i = 0
while true
if i % 2 == 1 or i == length - 1
return self[i]
else
i += yield(self[i]) ? 1 : 2
end
end
end
def ternary_flat_fast # by @illusionist
index = 0
index += 2 until (index >= self.length - 1) || yield(self[index])
return self[index+1] unless index == self.length - 1
self.last
end
def ternary_nested
find {|i| i.length == 1 or yield i[0]} .last
end
def ternary_options default # by @ReinHenrichs
find(default) {|i| yield i} .result
end
def case_when_then_else(&block) # by @Amadan
each_slice(2).find { |x|
x.size == 1 || (block_given? ? block[x[0]] : x[0])
}&.last
end
end
require 'benchmark'
Benchmark.bmbm(9) do |i|
i.report('flat slow') { 1000000.times { flat.ternary_flat_slow {|con| con == 'true' }}}
i.report('flat fast') { 1000000.times { flat.ternary_flat_fast {|con| con == 'true' }}}
i.report(' nested') { 1000000.times { nested.ternary_nested {|con| con == 'true' }}}
i.report(' options') { 1000000.times { options.ternary_options(Default.new(6)) {|con| con.condition == 'true' }}}
i.report(' c_w_t_e') { 1000000.times { flat.case_when_then_else {|con| con == 'true' }}}
end
结果
Rehearsal ---------------------------------------------
flat slow 4.510000 0.030000 4.540000 ( 4.549424) # original
flat fast 3.600000 0.030000 3.630000 ( 3.656058) # @illusionist
nested 6.920000 0.080000 7.000000 ( 7.252300) # me (as suggested)
options 7.030000 0.050000 7.080000 ( 7.130508) # @ReinHenrichs
c_w_t_e 19.320000 0.140000 19.460000 ( 19.593633) # @Amadan
----------------------------------- total: 41.710000sec
user system total real
flat slow 4.290000 0.030000 4.320000 ( 4.339875) # original
flat fast 3.360000 0.020000 3.380000 ( 3.401809) # @illusionist
nested 6.180000 0.040000 6.220000 ( 6.258939) # me (as suggested)
options 6.640000 0.040000 6.680000 ( 6.704991) # @ReinHenrichs
c_w_t_e 18.340000 0.120000 18.460000 ( 18.548176) # @Amadan
不过"unrubyish",@illusionist的回答是最快的,速度是首要问题
我认为这是一个XY Problem。您已将成对的值展平到一个数组中。这迫使您付出很多额外的努力来测试数组索引以恢复最初包含在对中的信息(哪个元素是第一个,哪个元素是第二个)。所有这些额外的工作都可以通过简单地不首先压平这些对来避免,从而保留他们对哪个元素是哪个的知识。
正因为如此,我将针对您要解决的问题提供解决方案,而不是告诉您如何实施您选择的解决方案。
您可以将每个 condition/result 对表示为一个对象,然后 find
第一个匹配的:
# A condition/result pair
Option = Struct.new :condition, :result
# To hold a default value which duck-types with `result`.
Default = Struct.new :result
options = [Option.new('false', 0), Option.new('true', 1), Option.new('true', 2)]
options.find(Default.new(3)) {|opt| opt.condition == 'true'}.result
# => 1
如果您对创建新对象过敏,可以成对使用 2 元素数组:
options = [['false', 0], ['true', 1], ['true', 2]]
options.find([nil, 3]) {|condition, result| condition == 'true'}.last
# => 1
但是Ruby是一种面向对象的语言。创建新对象来解决新问题正是该语言的设计目的。
我想说的是,这比您现有的解决方案更像 Rubyish,但 Rein 是对的 - 整个概念一开始就不是很像 Rubyish,您可能会从提出不同的、更像 Ruby 的问题中获益具体问题。
module ArrayWithCase
refine Array do
def case_when_then_else(&block)
each_slice(2).find { |x|
x.size == 1 || (block_given? ? block[x[0]] : x[0])
}&.last
end
end
end
module Test
using ArrayWithCase
p ['false', 0, 'true', 1, 'true', 2, 3].case_when_then_else {|i| i == 'true'}
# => 1
p [false, 0, true, 1, true, 2, 3].case_when_then_else
# => 1
p [1,2,3,4,5].case_when_then_else { |i| i > 6 }
# => 6
p ["only_possible_answer"].case_when_then_else(&:itself)
# => "only_possible_answer"
p [false, 4].case_when_then_else(&:itself)
# => nil
end
fastest among all answers
我的解决方案
require 'minitest/autorun'
class OldArray < Array
def ternary &block
# the block checks if a condition is true
i = 0
while true
if i % 2 == 1 or i == length - 1
return self[i]
else
i += yield(self[i]) ? 1 : 2
end
end
end
end
class NewArray < Array
def ternary
index = 0
# Loop until you find item satisfying the condition skipping one item
index += 2 until (index >= self.length - 1) || yield(self[index])
# return the next value unless its the last/default item
return self[index+1] unless index == self.length - 1
# return default item
self.last
end
end
class TestMe < Minitest::Test
def test_array
assert_equal 5, NewArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 6}
assert_equal 4, NewArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 2}
assert_equal 5, NewArray.new([1, 2, 3, 4, 5]).ternary {|i| puts "is: #{i}";i > 3}
assert_equal 1, NewArray.new(['false', 0, 'true', 1, 'true', 2, 3]).ternary {|i|i == 'true'}
end
end
require 'benchmark/ips'
Benchmark.ips do |x|
x.report("My Technique") { NewArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 6} }
x.report("Your Technique") { OldArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 6} }
end
基准
Warming up --------------------------------------
My Technique 98.295k i/100ms
Your Technique 73.008k i/100ms
Calculating -------------------------------------
My Technique 1.292M (± 1.9%) i/s - 6.487M in 5.023137s
Your Technique 891.204k (± 1.8%) i/s - 4.526M in 5.080896s
Note: I have created new classes from Array
for testing purposes. You can however open the Array
class and add the behavior. It will work as fine.
我将一些决策数据存储在数组中,如下所示:[condition, result, ..., condition, result, default]
,所以基本上是三元表达式 (c ? r : ... c ? r : d
),我正在用这段代码对它们进行评估:
class Array
def ternary &block
# the block checks if a condition is true
i = 0
while true
if i % 2 == 1 or i == length - 1
return self[i]
else
i += yield(self[i]) ? 1 : 2
end
end
end
end
['false', 0, 'true', 1, 'true', 2, 3].ternary {|i| i == 'true'}
# => 1
[1, 2, 3, 4, 5].ternary {|i| i > 6}
# => 5 (defaults to last value because all conditions failed)
我想知道是否有更快的内置方法或如何改进此代码。
注意:有任意多个条件,[only_possible_answer]
也应该有效
编辑:到目前为止的答案(测试了同一数组的 1 000 000 次迭代):
设置
flat = ['false', 0, 'false', 1, 'false', 2, 'false', 3, 'false', 4, 'true', 5, 6]
nested = [['false', 0], ['false', 1], ['false', 2], ['false', 3], ['false', 4], ['true', 5], [6]]
Option = Struct.new :condition, :result
Default = Struct.new :result
class Default
def call; self; end
# otherwise:
# undefined method ‘call’ for #<struct Default result=whatever>
end
options = [Option.new('false', 0), Option.new('false', 1), Option.new('false', 2), Option.new('false', 3), Option.new('false', 4), Option.new('true', 5)]
class Array
def ternary_flat_slow
i = 0
while true
if i % 2 == 1 or i == length - 1
return self[i]
else
i += yield(self[i]) ? 1 : 2
end
end
end
def ternary_flat_fast # by @illusionist
index = 0
index += 2 until (index >= self.length - 1) || yield(self[index])
return self[index+1] unless index == self.length - 1
self.last
end
def ternary_nested
find {|i| i.length == 1 or yield i[0]} .last
end
def ternary_options default # by @ReinHenrichs
find(default) {|i| yield i} .result
end
def case_when_then_else(&block) # by @Amadan
each_slice(2).find { |x|
x.size == 1 || (block_given? ? block[x[0]] : x[0])
}&.last
end
end
require 'benchmark'
Benchmark.bmbm(9) do |i|
i.report('flat slow') { 1000000.times { flat.ternary_flat_slow {|con| con == 'true' }}}
i.report('flat fast') { 1000000.times { flat.ternary_flat_fast {|con| con == 'true' }}}
i.report(' nested') { 1000000.times { nested.ternary_nested {|con| con == 'true' }}}
i.report(' options') { 1000000.times { options.ternary_options(Default.new(6)) {|con| con.condition == 'true' }}}
i.report(' c_w_t_e') { 1000000.times { flat.case_when_then_else {|con| con == 'true' }}}
end
结果
Rehearsal ---------------------------------------------
flat slow 4.510000 0.030000 4.540000 ( 4.549424) # original
flat fast 3.600000 0.030000 3.630000 ( 3.656058) # @illusionist
nested 6.920000 0.080000 7.000000 ( 7.252300) # me (as suggested)
options 7.030000 0.050000 7.080000 ( 7.130508) # @ReinHenrichs
c_w_t_e 19.320000 0.140000 19.460000 ( 19.593633) # @Amadan
----------------------------------- total: 41.710000sec
user system total real
flat slow 4.290000 0.030000 4.320000 ( 4.339875) # original
flat fast 3.360000 0.020000 3.380000 ( 3.401809) # @illusionist
nested 6.180000 0.040000 6.220000 ( 6.258939) # me (as suggested)
options 6.640000 0.040000 6.680000 ( 6.704991) # @ReinHenrichs
c_w_t_e 18.340000 0.120000 18.460000 ( 18.548176) # @Amadan
不过"unrubyish",@illusionist的回答是最快的,速度是首要问题
我认为这是一个XY Problem。您已将成对的值展平到一个数组中。这迫使您付出很多额外的努力来测试数组索引以恢复最初包含在对中的信息(哪个元素是第一个,哪个元素是第二个)。所有这些额外的工作都可以通过简单地不首先压平这些对来避免,从而保留他们对哪个元素是哪个的知识。
正因为如此,我将针对您要解决的问题提供解决方案,而不是告诉您如何实施您选择的解决方案。
您可以将每个 condition/result 对表示为一个对象,然后 find
第一个匹配的:
# A condition/result pair
Option = Struct.new :condition, :result
# To hold a default value which duck-types with `result`.
Default = Struct.new :result
options = [Option.new('false', 0), Option.new('true', 1), Option.new('true', 2)]
options.find(Default.new(3)) {|opt| opt.condition == 'true'}.result
# => 1
如果您对创建新对象过敏,可以成对使用 2 元素数组:
options = [['false', 0], ['true', 1], ['true', 2]]
options.find([nil, 3]) {|condition, result| condition == 'true'}.last
# => 1
但是Ruby是一种面向对象的语言。创建新对象来解决新问题正是该语言的设计目的。
我想说的是,这比您现有的解决方案更像 Rubyish,但 Rein 是对的 - 整个概念一开始就不是很像 Rubyish,您可能会从提出不同的、更像 Ruby 的问题中获益具体问题。
module ArrayWithCase
refine Array do
def case_when_then_else(&block)
each_slice(2).find { |x|
x.size == 1 || (block_given? ? block[x[0]] : x[0])
}&.last
end
end
end
module Test
using ArrayWithCase
p ['false', 0, 'true', 1, 'true', 2, 3].case_when_then_else {|i| i == 'true'}
# => 1
p [false, 0, true, 1, true, 2, 3].case_when_then_else
# => 1
p [1,2,3,4,5].case_when_then_else { |i| i > 6 }
# => 6
p ["only_possible_answer"].case_when_then_else(&:itself)
# => "only_possible_answer"
p [false, 4].case_when_then_else(&:itself)
# => nil
end
fastest among all answers
我的解决方案
require 'minitest/autorun'
class OldArray < Array
def ternary &block
# the block checks if a condition is true
i = 0
while true
if i % 2 == 1 or i == length - 1
return self[i]
else
i += yield(self[i]) ? 1 : 2
end
end
end
end
class NewArray < Array
def ternary
index = 0
# Loop until you find item satisfying the condition skipping one item
index += 2 until (index >= self.length - 1) || yield(self[index])
# return the next value unless its the last/default item
return self[index+1] unless index == self.length - 1
# return default item
self.last
end
end
class TestMe < Minitest::Test
def test_array
assert_equal 5, NewArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 6}
assert_equal 4, NewArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 2}
assert_equal 5, NewArray.new([1, 2, 3, 4, 5]).ternary {|i| puts "is: #{i}";i > 3}
assert_equal 1, NewArray.new(['false', 0, 'true', 1, 'true', 2, 3]).ternary {|i|i == 'true'}
end
end
require 'benchmark/ips'
Benchmark.ips do |x|
x.report("My Technique") { NewArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 6} }
x.report("Your Technique") { OldArray.new([1, 2, 3, 4, 5]).ternary {|i| i > 6} }
end
基准
Warming up --------------------------------------
My Technique 98.295k i/100ms
Your Technique 73.008k i/100ms
Calculating -------------------------------------
My Technique 1.292M (± 1.9%) i/s - 6.487M in 5.023137s
Your Technique 891.204k (± 1.8%) i/s - 4.526M in 5.080896s
Note: I have created new classes from
Array
for testing purposes. You can however open theArray
class and add the behavior. It will work as fine.