简体   繁体   中英

Difference between 3-dot-range operator and 2-dot-range operator in flip flop ruby

Please help me to understand the difference between range operators ... and .. as "flip-flops" used in Ruby.

This is an example from Pragmatic Programmers guide to Ruby:

a = (11..20).collect {|i| (i%4 == 0)..(i%3 == 0) ? i : nil}

which returns:

[nil, 12, nil, nil, nil, 16, 17, 18, nil, 20]

Also:

a = (11..20).collect {|i| (i%4 == 0)...(i%3 == 0) ? i : nil}

returned:

[nil, 12, 13, 14, 15, 16, 17, 18, nil, 20]

The flip/flop (aka f/f) is a stateful operator which originates from perl.

f/f operator is implied in conditional statements (if and ternary) in ruby instead of range so (1..5) is a range but (1..5) ? 1 : 5 is af/f. f/f has an internal state (true / false) and consists of two conditions. It tuns ON(state becomes true) when it's first condition evaluates to true and OFF when it's second condition evalutes to true. The difference between two and three dotted versions is that two-dotted evaluates second condition immediately after the first one evaluated to true and three-dotted doesn't.

two dotted version works like this:

A..B |
A -> false | State -> false
A -> true, B -> false | State -> true # notice how it checks both conditions
B -> false | State -> true
B -> true | State -> false
A -> false  | State -> false
A -> true, B -> true | State -> false

compare it to three dotted version

A...B
A -> false | State -> false
A -> true | State -> true # three dotted version doesn't check second condition immediately
B -> false | State -> true
B -> true | State -> false
A -> false | State -> false
A -> true | State -> true

Let's follow along the amazing perl article but with examples in ruby

Two dotted example:

DATA.each_line do |line|
  print "\t" if (line =~ /^start/ .. line =~ /^end/)
  print line
end

__END__
First line.
start
Indented line
end
Back to left margin

This prints:

First line.
    start
    Indented line
    end
Back to left margin

as you can see - f/f turns ON on line 2 and OFF on line 4. Yet there is a subtlety to it. Check this out:

DATA.each_line do |line|
  print "\t" if (line =~ /start/ .. line =~ /end/)
  print line
end
__END__
First line.
Indent lines between the start and the end markers
Back to left margin

This prints:

First line.
    Indent lines between the start and the end markers
Back to left margin

There it turns ON on line 2 and immediately turns OFF.

Let's imagine that you don't want to check the second operator immediately after the first. It's where three-dotted f/f becomes handy. Check out the next example.

DATA.each_line do |line|
  print "\t" if (line =~ /start/ ... line =~ /end/)
  print line
end

__END__
First line.
Indent lines between the start and the end markers
So this is indented,
and this is the end of the indented block.
Back to left margin

Which prints:

First line.
    Indent lines between the start and the end markers
    So this is indented,
    and this is the end of the indented block.
Back to left margin

as you can see it turns ON on line 2 and OFF on line 4

Now let's apply this to your examples

I wrote a small script to illustrate it's behaviour

def mod(n, i)
  result = i % n == 0
  puts "#{i} mod #{n} => #{result}"
  result
end

(11..20).each { |i|
  if (mod(4, i))...(mod(3, i)) # NOTE it's a three dotted version
    # NOTE that those puts show previous state, not the current one!
    puts true
  else
    puts false
  end
}

two dotted result:

11 mod 4 => false
false
12 mod 4 => true
12 mod 3 => true # Notice how it checks both conditions here
true
13 mod 4 => false
false
14 mod 4 => false
false
15 mod 4 => false
false
16 mod 4 => true
16 mod 3 => false
true
17 mod 3 => false
true
18 mod 3 => true
true
19 mod 4 => false
false
20 mod 4 => true
20 mod 3 => false
true

three dotted result:

11 mod 4 => false
false
12 mod 4 => true
true
13 mod 3 => false
true
14 mod 3 => false
true
15 mod 3 => true # turns OFF here
true
16 mod 4 => true # and turns immediately ON here
true
17 mod 3 => false
true
18 mod 3 => true
true
19 mod 4 => false
false
20 mod 4 => true
true
=> 11..20

PS Range and flip/flop are two completely distinct operators and you should not mix them up.

The concept of flip-flop switch actually came from electronics . The main advantage of it is that it remembers it's state. Consider flip-flop boolean range as a variable, storing boolean value. Let's take a look at following example:

1.upto(10).each do |i|
  puts i if (i%2==0)..(i%4==0)
end

        #                        vvvv   value of “hidden” state
2       # left range boundary  ⇨ true
3
4       # right range boundary ⇨ false, no 5 follows 
6       # left range boundary  ⇨ true
7
8       # right range boundary ⇨ false, no 9 follows 
10

In your first example the “condition” became true for 4-multiples and turns back to false on 3-multiples. In the second example the condition variable was not turned off on 12 just because the right range boundary ( i%3 ) is excluded , since it's a 3-dot-range.

Hope the example was not too tangled.

The difference between 2 dots and 3 dots in Ruby is inclusion. For example

(1..100)
=> All numbers starting from 1 and ending at 100 INCLUDING 100

(1...100)
=> All numbers starting from 1 that are less than 100

(1..100).include?(100)
=> true

(1...100).include?(100)
=> false

Hope this helps.

When a double dot is used in a range it creates a range of numbers which go up to and includes the maximum number passed in. When a triple dot is used it creates a range which goes up to but does NOT include the maximum number passed in.

So:

 (1..5).each {| i | puts i} #will print 1,2,3,4,5

While:

(1...5).each {| i | puts i} #will print 1,2,3,4

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM