分配分支条件大小太高
Assignment Branch Condition size is too high
我正在制作采用多行字符串(日志)并将新字符串写入数组的方法。
def task_2(str)
result = []
str.each_line do |x|
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
if !ip.nil? && !datetime.nil? && !address.nil?
result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
end
end
result
end
我需要它通过默认配置的 rubocop 分析,但它给出 AbcSize 18.68/15
而且我确定是因为 if..end
声明,但我该如何重写它?
日志示例:
10.6.246.103 - - [23/Apr/2018:20:30:39 +0300] "POST /test/2/messages HTTP/1.1" 200 48 0.0498
10.6.246.101 - - [23/Apr/2018:20:30:42 +0300] "POST /test/2/run HTTP/1.1" 200 - 0.2277
我不使用 rubocop,但我确实用这些数据测试了以下内容:
data = <<FILE
10.6.246.103 - - [23/Apr/2018:20:30:39 +0300] "POST /test/2/messages HTTP/1.1" 200 48 0.0498
10.6.246.101 - - [23/Apr/2018:20:30:42 +0300] "POST /test/2/run HTTP/1.1" 200 - 0.2277
12.55.123.255 - - Hello
FILE
使用 String#gsub!
和 Enumerable#select
(报告 AbcSize 为 3)
def task_2(str)
str.each_line.select do |x|
# Without named groups
# x.gsub!(/\A([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*\z/m,
# ' FROM TO ')
x.gsub!(/\A(?<ip>[\d+\.\d+]+).*(?<=\[)(?<date_time>.*)(?=\]).*(?<=\s)(?<address>(?:\/\w+)*?)(?=\s).*\z/m,
'\k<date_time> FROM \k<ip> TO \k<address>')
end
end
task_2(data)
# => ["23/Apr/2018:20:30:39 +0300 FROM 10.6.246.103 TO /test/2/messages",
# "23/Apr/2018:20:30:42 +0300 FROM 10.6.246.101 TO /test/2/run"]
这里我们使用 String#gsub!
和模式替换,如果不进行替换,它将 return nil
从而从 Enumerable#select
中拒绝它。
类似的解决方案,尽管效率可能较低,使用 String#match
、Enumerable#map
和 Array#compact
(报告 AbcSize 为 7.14)
def task_2(str)
str.each_line.map do |x|
match = x.match(/\A(?<ip>[\d+\.\d+]+).*(?<=\[)(?<date_time>.*)(?=\]).*(?<=\s)(?<address>(?:\/\w+)*?)(?=\s)/)
"#{match['date_time']} FROM #{match['ip']} TO #{match['address']}" if match
end.compact
end
这里我们使用String#match
提取匹配数据,然后确认匹配,如果匹配则输出所需的格式。不匹配的字符串将输出 nil
,因此我们 compact
Array
删除 nil
值。
另一种选择可以是一次性 scan
整个 String
并分解匹配组:(报告 AbcSize 为 5)
def task_2(str)
str.scan(/^([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*$/)
.map {|a| "#{a[1]} FROM #{a[0]} TO #{a[2]}"}
end
可以通过
使最后一个低至2.24
def task_2(str)
r = []
str.scan(/^([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*$/) do |ip, date_time, address |
r << "#{date_time} FROM #{ip} TO #{address}"
end
r
end
def task_2(str)
result = []
str.each_line do |x|
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
if ip && datetime && address
result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
end
end
result
end
有!variable.nil?是多余的。基本上,你在这里检查存在,所以#present?方法就足够了,但是任何不是 nil 或 false 的值都被认为是 false,所以为了更符合习惯,最好只使用我在 if 语句中使用的形式。这解决了 ABS 问题。
ABC 大小的计算方法如下:
√(assignments² + branches² + conditionals²)
先来看看作业:
result = []
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
这给我们留下了 4 个作业。
接下来是树枝。为此,我不得不提到大多数运算符都是方法(因此计入分支)例如 1 + 1
也可以写成 1.+(1)
+
是整数上的方法。这同样适用于 string[regex]
,可以写成 string.[](regex)
[]
是字符串上的一种方法。 !value
可以写成 value.!@
!@
是所有对象的方法。
让我们数一数分支。
str.each_line
x[/^.* - -/]
x[/[\[].*[\]]/]
x[/T .* H/]
!ip.nil? # counts for 2 (! and .nil?)
!datetime.nil? # counts for 2 (! and .nil?)
!address.nil? # counts for 2 (! and .nil?)
result << ...
datetime[1..-2]
ip[0..-4]
address[1..-3]
+ # 4 times in result << ... + ... + ....
这给我们留下了 18 个分支。
最后要计算的是条件句。由于 Ruby 使用 &&
和 ||
运算符的短路,它们将计入条件。
if
&& # 2 times
这给我们留下了 3 个条件。
√(4² + 18² + 3²) ≈ 18.68
现在我们了解了数字的来源,我们可以尝试减少它。减少 ABC 大小的最简单方法是减少数字最大的东西,因为这个数字是平方的。在您的情况下,这些是分支机构。您已经发现了问题所在。
if !ip.nil? && !datetime.nil? && !address.nil?
result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
end
可以简化为:
if ip && datetime && address
result << "#{datetime[1..-2]} FROM: #{ip[0..-4]}TO:#{address[1..-3]}"
end
一共拿走10个树枝。 3 次 !something.nil?
(算 2 次,因为 !
和 .nil?
都计入分支)和 4 次 +
.
留给你:
√(4² + 8² + 3²) ≈ 9.43
任何时候我 运行 进入 ABC 太高(或类似的 complexity/length 警告),我很快就会把方法砍掉。您的可读性、可测试性和可维护性几乎总是会提高。
最快的方法是将循环体或条件提取到新方法中。根据需要重复,直到您可以一口气阅读每个方法。
同样,如果您有大型复杂的 conditionals/loop 构造,也请将其提取到新方法中。
将这两种策略组合足够多次,可以将任何方法简化为大致两个方法调用。在某些情况下,这可能有点过分热心……但永远不会太远。
这是您可以将该策略应用于您的代码的一种方法:
def task_2(str)
result = []
str.each_line do |x|
ip, datetime, address = parse_line(x)
if [ip, datetime, address].all?
result << "#{datetime[1..-2]} FROM: #{ip[0..-4]} TO: #{address[1..-3]}"
end
end
result
end
def parse_line(x)
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
return [ip, datetime, address]
end
s =<<EOF
123.123.123.999 - - [2009-12-31 13:13:13] T www.google.com H"
456.456.456.999 - - [2009-12-31 13:13:13] 404"
678.678.678.999 - - [2009-12-31 13:13:13] T www.amazon.com H"
EOF
puts task_2(s)
产生输出:
2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com
2009-12-31 13:13:13 FROM: 678.678.678.999 TO: www.amazon.com
如果你想走得更远,你可以将 each_line
的主体拉出到一个新方法,process_line
,等等。如果你创建了一个 class,你可以避免混乱的(在我看来)多值 returns。
这是一个使用命名捕获组很方便的问题。
R = /
(?= # begin a positive lookahead
(?<ip>.*\s-\s-) # match the string in a capture group named 'ip'
) # end positive lookahead
(?= # begin a positive lookahead
.* # match any number of characters
(?<datetime>[\[].*[\]]) # match the string in a capture group named 'datetime'
) # end positive lookahead
(?= # begin a positive lookahead
.* # match any number of characters
(?<address>T\s.*\sH) # match the string in a capture group named 'address'
) # end positive lookahead
/x # free-spacing regex definition mode
def task_2(str)
str.each_line.with_object([]) do |s, result|
m = str.match(R)
result << m[:datetime][1..-2] + ' FROM: ' + m[:ip][0..-4] +
'TO:' + m[:address][1..-3] unless m.nil?
end
end
str =<<_
123.123.123.999 - - [2009-12-31 13:13:13] T www.google.com H"
456.456.456.999 - - [2009-12-31 13:13:13] 404"
678.678.678.999 - - [2009-12-31 13:13:13] T www.amazon.com
_
task_2 str
#=> ["2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com",
# "2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com",
# "2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com"]
正则表达式约定俗成如下
R = /(?=(?<ip>\A.* - -))(?=.*(?<datetime>[\[].*[\]]))(?=.*(?<address>T .* H))/
请注意,在以自由间距模式编写正则表达式时,我在这里有空格的地方有空白字符 (\s
)。那是因为在自由间距模式下,在计算表达式之前会去除空格。或者,可以通过将空格括在字符 类 ([ ]
).
中以自由间距模式保留空格。
我正在制作采用多行字符串(日志)并将新字符串写入数组的方法。
def task_2(str)
result = []
str.each_line do |x|
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
if !ip.nil? && !datetime.nil? && !address.nil?
result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
end
end
result
end
我需要它通过默认配置的 rubocop 分析,但它给出 AbcSize 18.68/15
而且我确定是因为 if..end
声明,但我该如何重写它?
日志示例:
10.6.246.103 - - [23/Apr/2018:20:30:39 +0300] "POST /test/2/messages HTTP/1.1" 200 48 0.0498
10.6.246.101 - - [23/Apr/2018:20:30:42 +0300] "POST /test/2/run HTTP/1.1" 200 - 0.2277
我不使用 rubocop,但我确实用这些数据测试了以下内容:
data = <<FILE
10.6.246.103 - - [23/Apr/2018:20:30:39 +0300] "POST /test/2/messages HTTP/1.1" 200 48 0.0498
10.6.246.101 - - [23/Apr/2018:20:30:42 +0300] "POST /test/2/run HTTP/1.1" 200 - 0.2277
12.55.123.255 - - Hello
FILE
使用 String#gsub!
和 Enumerable#select
(报告 AbcSize 为 3)
def task_2(str)
str.each_line.select do |x|
# Without named groups
# x.gsub!(/\A([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*\z/m,
# ' FROM TO ')
x.gsub!(/\A(?<ip>[\d+\.\d+]+).*(?<=\[)(?<date_time>.*)(?=\]).*(?<=\s)(?<address>(?:\/\w+)*?)(?=\s).*\z/m,
'\k<date_time> FROM \k<ip> TO \k<address>')
end
end
task_2(data)
# => ["23/Apr/2018:20:30:39 +0300 FROM 10.6.246.103 TO /test/2/messages",
# "23/Apr/2018:20:30:42 +0300 FROM 10.6.246.101 TO /test/2/run"]
这里我们使用 String#gsub!
和模式替换,如果不进行替换,它将 return nil
从而从 Enumerable#select
中拒绝它。
类似的解决方案,尽管效率可能较低,使用 String#match
、Enumerable#map
和 Array#compact
(报告 AbcSize 为 7.14)
def task_2(str)
str.each_line.map do |x|
match = x.match(/\A(?<ip>[\d+\.\d+]+).*(?<=\[)(?<date_time>.*)(?=\]).*(?<=\s)(?<address>(?:\/\w+)*?)(?=\s)/)
"#{match['date_time']} FROM #{match['ip']} TO #{match['address']}" if match
end.compact
end
这里我们使用String#match
提取匹配数据,然后确认匹配,如果匹配则输出所需的格式。不匹配的字符串将输出 nil
,因此我们 compact
Array
删除 nil
值。
另一种选择可以是一次性 scan
整个 String
并分解匹配组:(报告 AbcSize 为 5)
def task_2(str)
str.scan(/^([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*$/)
.map {|a| "#{a[1]} FROM #{a[0]} TO #{a[2]}"}
end
可以通过
使最后一个低至2.24 def task_2(str)
r = []
str.scan(/^([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*$/) do |ip, date_time, address |
r << "#{date_time} FROM #{ip} TO #{address}"
end
r
end
def task_2(str)
result = []
str.each_line do |x|
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
if ip && datetime && address
result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
end
end
result
end
有!variable.nil?是多余的。基本上,你在这里检查存在,所以#present?方法就足够了,但是任何不是 nil 或 false 的值都被认为是 false,所以为了更符合习惯,最好只使用我在 if 语句中使用的形式。这解决了 ABS 问题。
ABC 大小的计算方法如下:
√(assignments² + branches² + conditionals²)
先来看看作业:
result = []
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
这给我们留下了 4 个作业。
接下来是树枝。为此,我不得不提到大多数运算符都是方法(因此计入分支)例如 1 + 1
也可以写成 1.+(1)
+
是整数上的方法。这同样适用于 string[regex]
,可以写成 string.[](regex)
[]
是字符串上的一种方法。 !value
可以写成 value.!@
!@
是所有对象的方法。
让我们数一数分支。
str.each_line
x[/^.* - -/]
x[/[\[].*[\]]/]
x[/T .* H/]
!ip.nil? # counts for 2 (! and .nil?)
!datetime.nil? # counts for 2 (! and .nil?)
!address.nil? # counts for 2 (! and .nil?)
result << ...
datetime[1..-2]
ip[0..-4]
address[1..-3]
+ # 4 times in result << ... + ... + ....
这给我们留下了 18 个分支。
最后要计算的是条件句。由于 Ruby 使用 &&
和 ||
运算符的短路,它们将计入条件。
if
&& # 2 times
这给我们留下了 3 个条件。
√(4² + 18² + 3²) ≈ 18.68
现在我们了解了数字的来源,我们可以尝试减少它。减少 ABC 大小的最简单方法是减少数字最大的东西,因为这个数字是平方的。在您的情况下,这些是分支机构。您已经发现了问题所在。
if !ip.nil? && !datetime.nil? && !address.nil?
result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
end
可以简化为:
if ip && datetime && address
result << "#{datetime[1..-2]} FROM: #{ip[0..-4]}TO:#{address[1..-3]}"
end
一共拿走10个树枝。 3 次 !something.nil?
(算 2 次,因为 !
和 .nil?
都计入分支)和 4 次 +
.
留给你:
√(4² + 8² + 3²) ≈ 9.43
任何时候我 运行 进入 ABC 太高(或类似的 complexity/length 警告),我很快就会把方法砍掉。您的可读性、可测试性和可维护性几乎总是会提高。
最快的方法是将循环体或条件提取到新方法中。根据需要重复,直到您可以一口气阅读每个方法。
同样,如果您有大型复杂的 conditionals/loop 构造,也请将其提取到新方法中。
将这两种策略组合足够多次,可以将任何方法简化为大致两个方法调用。在某些情况下,这可能有点过分热心……但永远不会太远。
这是您可以将该策略应用于您的代码的一种方法:
def task_2(str)
result = []
str.each_line do |x|
ip, datetime, address = parse_line(x)
if [ip, datetime, address].all?
result << "#{datetime[1..-2]} FROM: #{ip[0..-4]} TO: #{address[1..-3]}"
end
end
result
end
def parse_line(x)
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]
return [ip, datetime, address]
end
s =<<EOF
123.123.123.999 - - [2009-12-31 13:13:13] T www.google.com H"
456.456.456.999 - - [2009-12-31 13:13:13] 404"
678.678.678.999 - - [2009-12-31 13:13:13] T www.amazon.com H"
EOF
puts task_2(s)
产生输出:
2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com
2009-12-31 13:13:13 FROM: 678.678.678.999 TO: www.amazon.com
如果你想走得更远,你可以将 each_line
的主体拉出到一个新方法,process_line
,等等。如果你创建了一个 class,你可以避免混乱的(在我看来)多值 returns。
这是一个使用命名捕获组很方便的问题。
R = /
(?= # begin a positive lookahead
(?<ip>.*\s-\s-) # match the string in a capture group named 'ip'
) # end positive lookahead
(?= # begin a positive lookahead
.* # match any number of characters
(?<datetime>[\[].*[\]]) # match the string in a capture group named 'datetime'
) # end positive lookahead
(?= # begin a positive lookahead
.* # match any number of characters
(?<address>T\s.*\sH) # match the string in a capture group named 'address'
) # end positive lookahead
/x # free-spacing regex definition mode
def task_2(str)
str.each_line.with_object([]) do |s, result|
m = str.match(R)
result << m[:datetime][1..-2] + ' FROM: ' + m[:ip][0..-4] +
'TO:' + m[:address][1..-3] unless m.nil?
end
end
str =<<_
123.123.123.999 - - [2009-12-31 13:13:13] T www.google.com H"
456.456.456.999 - - [2009-12-31 13:13:13] 404"
678.678.678.999 - - [2009-12-31 13:13:13] T www.amazon.com
_
task_2 str
#=> ["2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com",
# "2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com",
# "2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com"]
正则表达式约定俗成如下
R = /(?=(?<ip>\A.* - -))(?=.*(?<datetime>[\[].*[\]]))(?=.*(?<address>T .* H))/
请注意,在以自由间距模式编写正则表达式时,我在这里有空格的地方有空白字符 (\s
)。那是因为在自由间距模式下,在计算表达式之前会去除空格。或者,可以通过将空格括在字符 类 ([ ]
).