Related
I was going through the exercises in Ruby Koans and I was struck by the following Ruby quirk that I found really unexplainable:
array = [:peanut, :butter, :and, :jelly]
array[0] #=> :peanut #OK!
array[0,1] #=> [:peanut] #OK!
array[0,2] #=> [:peanut, :butter] #OK!
array[0,0] #=> [] #OK!
array[2] #=> :and #OK!
array[2,2] #=> [:and, :jelly] #OK!
array[2,20] #=> [:and, :jelly] #OK!
array[4] #=> nil #OK!
array[4,0] #=> [] #HUH?? Why's that?
array[4,100] #=> [] #Still HUH, but consistent with previous one
array[5] #=> nil #consistent with array[4] #=> nil
array[5,0] #=> nil #WOW. Now I don't understand anything anymore...
So why is array[5,0] not equal to array[4,0]? Is there any reason why array slicing behaves this weird when you start at the (length+1)th position??
Slicing and indexing are two different operations, and inferring the behaviour of one from the other is where your problem lies.
The first argument in slice identifies not the element but the places between elements, defining spans (and not elements themselves):
:peanut :butter :and :jelly
0 1 2 3 4
4 is still within the array, just barely; if you request 0 elements, you get the empty end of the array. But there is no index 5, so you can't slice from there.
When you do index (like array[4]), you are pointing at elements themselves, so the indices only go from 0 to 3.
this has to do with the fact that slice returns an array, relevant source documentation from Array#slice:
* call-seq:
* array[index] -> obj or nil
* array[start, length] -> an_array or nil
* array[range] -> an_array or nil
* array.slice(index) -> obj or nil
* array.slice(start, length) -> an_array or nil
* array.slice(range) -> an_array or nil
which suggests to me that if you give the start that is out of bounds, it will return nil, thus in your example array[4,0] asks for the 4th element that exists, but asks to return an array of zero elements. While array[5,0] asks for an index out of bounds so it returns nil. This perhaps makes more sense if you remember that the slice method is returning a new array, not altering the original data structure.
EDIT:
After reviewing the comments I decided to edit this answer. Slice calls the following code snippet when the arg value is two:
if (argc == 2) {
if (SYMBOL_P(argv[0])) {
rb_raise(rb_eTypeError, "Symbol as array index");
}
beg = NUM2LONG(argv[0]);
len = NUM2LONG(argv[1]);
if (beg < 0) {
beg += RARRAY(ary)->len;
}
return rb_ary_subseq(ary, beg, len);
}
if you look in the array.c class where the rb_ary_subseq method is defined, you see that it is returning nil if the length is out of bounds, not the index:
if (beg > RARRAY_LEN(ary)) return Qnil;
In this case this is what is happening when 4 is passed in, it checks that there are 4 elements and thus does not trigger the nil return. It then goes on and returns an empty array if the second arg is set to zero. while if 5 is passed in, there are not 5 elements in the array, so it returns nil before the zero arg is evaluated. code here at line 944.
I believe this to be a bug, or at least unpredictable and not the 'Principle of Least Surprise'. When I get a few minutes I will a least submit a failing test patch to ruby core.
At least note that the behavior is consistent. From 5 on up everything acts the same; the weirdness only occurs at [4,N].
Maybe this pattern helps, or maybe I'm just tired and it doesn't help at all.
array[0,4] => [:peanut, :butter, :and, :jelly]
array[1,3] => [:butter, :and, :jelly]
array[2,2] => [:and, :jelly]
array[3,1] => [:jelly]
array[4,0] => []
At [4,0], we catch the end of the array. I'd actually find it rather odd, as far as beauty in patterns go, if the last one returned nil. Because of a context like this, 4 is an acceptable option for the first parameter so that the empty array can be returned. Once we hit 5 and up, though, the method likely exits immediately by nature of being totally and completely out of bounds.
This makes sense when you consider than an array slice can be a valid lvalue, not just an rvalue:
array = [:peanut, :butter, :and, :jelly]
# replace 0 elements starting at index 5 (insert at end or array):
array[4,0] = [:sandwich]
# replace 0 elements starting at index 0 (insert at head of array):
array[0,0] = [:make, :me, :a]
# array is [:make, :me, :a, :peanut, :butter, :and, :jelly, :sandwich]
# this is just like replacing existing elements:
array[3, 4] = [:grilled, :cheese]
# array is [:make, :me, :a, :grilled, :cheese, :sandwich]
This wouldn't be possible if array[4,0] returned nil instead of []. However, array[5,0] returns nil because it's out of bounds (inserting after the 4th element of a 4-element array is meaningful, but inserting after the 5th element of a 4 element array is not).
Read the slice syntax array[x,y] as "starting after x elements in array, select up to y elements". This is only meaningful if array has at least x elements.
This does make sense
You need to be able to assign to those slices, so they are defined in such a way that the beginning and the end of the string have working zero-length expressions.
array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
I found explanation by Gary Wright very helpful as well.
http://www.ruby-forum.com/topic/1393096#990065
The answer by Gary Wright is -
http://www.ruby-doc.org/core/classes/Array.html
The docs certainly could be more clear but the actual behavior is
self-consistent and useful.
Note: I'm assuming 1.9.X version of String.
It helps to consider the numbering in the following way:
-4 -3 -2 -1 <-- numbering for single argument indexing
0 1 2 3
+---+---+---+---+
| a | b | c | d |
+---+---+---+---+
0 1 2 3 4 <-- numbering for two argument indexing or start of range
-4 -3 -2 -1
The common (and understandable) mistake is too assume that the semantics
of the single argument index are the same as the semantics of the
first argument in the two argument scenario (or range). They are not
the same thing in practice and the documentation doesn't reflect this.
The error though is definitely in the documentation and not in the
implementation:
single argument: the index represents a single character position
within the string. The result is either the single character string
found at the index or nil because there is no character at the given
index.
s = ""
s[0] # nil because no character at that position
s = "abcd"
s[0] # "a"
s[-4] # "a"
s[-5] # nil, no characters before the first one
two integer arguments: the arguments identify a portion of the string to
extract or to replace. In particular, zero-width portions of the string
can also be identified so that text can be inserted before or after
existing characters including at the front or end of the string. In this
case, the first argument does not identify a character position but
instead identifies the space between characters as shown in the diagram
above. The second argument is the length, which can be 0.
s = "abcd" # each example below assumes s is reset to "abcd"
To insert text before 'a': s[0,0] = "X" # "Xabcd"
To insert text after 'd': s[4,0] = "Z" # "abcdZ"
To replace first two characters: s[0,2] = "AB" # "ABcd"
To replace last two characters: s[-2,2] = "CD" # "abCD"
To replace middle two characters: s[1..3] = "XX" # "aXXd"
The behavior of a range is pretty interesting. The starting point is the
same as the first argument when two arguments are provided (as described
above) but the end point of the range can be the 'character position' as
with single indexing or the "edge position" as with two integer
arguments. The difference is determined by whether the double-dot range
or triple-dot range is used:
s = "abcd"
s[1..1] # "b"
s[1..1] = "X" # "aXcd"
s[1...1] # ""
s[1...1] = "X" # "aXbcd", the range specifies a zero-width portion of
the string
s[1..3] # "bcd"
s[1..3] = "X" # "aX", positions 1, 2, and 3 are replaced.
s[1...3] # "bc"
s[1...3] = "X" # "aXd", positions 1, 2, but not quite 3 are replaced.
If you go back through these examples and insist and using the single
index semantics for the double or range indexing examples you'll just
get confused. You've got to use the alternate numbering I show in the
ascii diagram to model the actual behavior.
I agree that this seems like strange behavior, but even the official documentation on Array#slice demonstrates the same behavior as in your example, in the "special cases" below:
a = [ "a", "b", "c", "d", "e" ]
a[2] + a[0] + a[1] #=> "cab"
a[6] #=> nil
a[1, 2] #=> [ "b", "c" ]
a[1..3] #=> [ "b", "c", "d" ]
a[4..7] #=> [ "e" ]
a[6..10] #=> nil
a[-3, 3] #=> [ "c", "d", "e" ]
# special cases
a[5] #=> nil
a[5, 1] #=> []
a[5..10] #=> []
Unfortunately, even their description of Array#slice doesn't seem to offer any insight as to why it works this way:
Element Reference—Returns the element at index, or returns a subarray starting at start and continuing for length elements, or returns a subarray specified by range. Negative indices count backward from the end of the array (-1 is the last element). Returns nil if the index (or starting index) are out of range.
An explanation provided by Jim Weirich
One way to think about it is that index position 4 is at the very edge
of the array. When asking for a slice, you return as much of the
array that is left. So consider the array[2,10], array[3,10] and
array[4,10] ... each returns the remaining bits of the end of the
array: 2 elements, 1 element and 0 elements respectively. However,
position 5 is clearly outside the array and not at the edge, so
array[5,10] returns nil.
Consider the following array:
>> array=["a","b","c"]
=> ["a", "b", "c"]
You can insert an item to the begining (head) of the array by assigning it to a[0,0]. To put the element between "a" and "b", use a[1,0]. Basically, in the notation a[i,n], i represents an index and n a number of elements. When n=0, it defines a position between the elements of the array.
Now if you think about the end of the array, how can you append an item to its end using the notation described above? Simple, assign the value to a[3,0]. This is the tail of the array.
So, if you try to access the element at a[3,0], you will get []. In this case you are still in the range of the array. But if you try to access a[4,0], you'll get nil as return value, since you're not within the range of the array anymore.
Read more about it at http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .
tl;dr: in the source code in array.c, different functions are called depending on whether you pass 1 or 2 arguments in to Array#slice resulting in the unexpected return values.
(First off, I'd like to point out that I don't code in C, but have been using Ruby for years. So if you're not familiar with C, but you take a few minutes to familiarize yourself with the basics of functions and variables it's really not that hard to follow the Ruby source code, as demonstrated below. This answer is based on Ruby v2.3, but is more or less the same back to v1.9.)
Scenario #1
array.length == 4; array.slice(4) #=> nil
If you look at the source code for Array#slice (rb_ary_aref), you see that when only one argument is passed in (lines 1277-1289), rb_ary_entry is called, passing in the index value (which can be positive or negative).
rb_ary_entry then calculates the position of the requested element from the beginning of the array (in other words, if a negative index is passed in, it computes the positive equivalent) and then calls rb_ary_elt to get the requested element.
As expected, rb_ary_elt returns nil when the length of the array len is less than or equal to the index (here called offset).
1189: if (offset < 0 || len <= offset) {
1190: return Qnil;
1191: }
Scenario #2
array.length == 4; array.slice(4, 0) #=> []
However when 2 arguments are passed in (i.e. the starting index beg, and length of the slice len), rb_ary_subseq is called.
In rb_ary_subseq, if the starting index beg is greater than the array length alen, nil is returned:
1208: long alen = RARRAY_LEN(ary);
1209:
1210: if (beg > alen) return Qnil;
Otherwise the length of the resulting slice len is calculated, and if it's determined to be zero, an empty array is returned:
1213: if (alen < len || alen < beg + len) {
1214: len = alen - beg;
1215: }
1216: klass = rb_obj_class(ary);
1217: if (len == 0) return ary_new(klass, 0);
So since the starting index of 4 is not greater than array.length, an empty array is returned instead of the nil value that one might expect.
Question answered?
If the actual question here isn't "What code causes this to happen?", but rather, "Why did Matz do it this way?", well you'll just have to buy him a cup of coffee at the next RubyConf and ask him.
ary = [1, 4, 6, 9]
(0...ary.size).bsearch { |i|
ary[i] - 1
} # => nil
1 - ary[i] # => 0
When the code is written in a form ary[i] - 1 which doesn't work as expected.
What I am trying to do is to find the index of the number 1 in the array.
But 1 - ary[i] can return the number's index correctly. Why doesn't ary[i] - 1 work?
Array#bsearch is meant to perform binary search to find an element that meets certain criteria. As per documentation, if you return numeric values from the block, the find-any mode type of search is used.
The search starts at center of the sorted array - and if block returns negative value, it continues search in first half, and if block returns positive value, it continues the search in second half of the array.
In your case when you use ary[i] - 1, the value returned by block is always positive and search continues recursively on second half of the array - and never finds the value 1.
Here is the code with some debug statements:
ary = [1, 4, 6, 9]
p (0...ary.size).bsearch { |i|
puts "Elem: #{ary[i]} Index: #{i}"
ary[i] - 1
}
Output:
Elem: 4 Index: 1
Elem: 6 Index: 2
Elem: 9 Index: 3
nil
[Finished in 0.4s]
Array#bsearch returns an element of the array, not the index of a matching element.
You might want to use Array#index instead.
If you want to find the index of the element instead of the element itself, you need to use Array#bsearch_index. Note: this method was introduced in Ruby 2.3, which at the time of this writing has not been released yet (it will be released on Christmas 2015).
The feature request for Array#bsearch_index contains a comment by Yusuke Endoh showing how to implement Array#bsearch_index (and in fact Array#bsearch as well) based on Range#bsearch:
class Array
def bsearch_index(&blk)
return enum_for(__method__) unless blk
(0...size).bsearch {|i| yield self[i] }
end
end
When either running Ruby 2.3 or using the above monkey patch, you can then do:
ary.bsearch_index(&1.method(:-))
in order to find the index of the 1 element in your array.
The reason why it doesn't work with
ary.bsearch_index {|el | el - 1 }
is simple: the block violates the contract of bsearch_index (and also bsearch since they are the same). The block needs to return a positive number for indices left of the one you are searching for, a negative number for indices right of the one you are searching for, and 0 for indices within the range you are searching for. Your block does the opposite.
I tried to see how Array#[]= works, and played around:
enum[int] = obj → obj
enum[start, length] = obj → obj
enum[range] = obj → obj
Question 1
I have one array b holding nil at its 0 index.
b = []
b[0] # => nil
I tried to replace nil with integer 10 in the code below.
b[-1] = 10 # => IndexError: index -1 too small for array; minimum: 0
Why doesn't the code above work, but the ones below do? In case of an array with size 1, why are the indices 0 and -1 treated differently?
b[0] = 5 # => 5
b[-1] = 10 # => 10
Question 2
I created an array of size 2, and did the following:
a = [1,2]
a[-3] = 3 # => IndexError: index -3 too small for array; minimum: -2
a[-3] = [3] # => IndexError: index -3 too small for array; minimum: -2
a[-3..-4] = [3] # => RangeError: -3..-4 out of range
I believe that negative index never increases the size of an array, but I don't know why. Why did the code below succeed?
a[-2..-3] = [3,4] #=> [3, 4]
I would suggest you to take a look at the first para in Array documentation. It surprisingly says: “A negative index is assumed to be relative to the end of the array—that is, an index of -1 indicates the last element of the array, -2 is the next to last element in the array, and so on.”
That means, that you may set a[-N]th element if and only |N| <= a.size. That’s why a = [1,2] ; a[-3] = 3 fails (3 > 2).
On the other hand, there is [likely] not documented feature for ruby arrays: a[INBOUNDS_IDX..NONSENSE_IDX]=SMTH will insert SMTH before INBOUNDS_IDX index:
a=[1,2]
a[2..0]='a'
a[2..1]='b'
a[2..-100]='c'
# ⇒ [1, 2, "c", "b", "a"]
a[2..-1]='q'
# ⇒ [1, 2, "q"]
Nonsense here means “less than INBOUNDS_IDX, and not treatable as index in an negative notation” (that’s why a[2..-1] in the example above is treated as a[2..(a.size - 1)].)
Q1:
An empty array has 0 elements, so when you try to set its element 0, with negative index -1, it will give an error.
Because negative index cycles through the array from the end.
So
a = []; a[-1] = 3 makes it impossible to
a) get the element at last position, since its null
b) set its value. since it was never captured.
a[0] = 5 will work because you are telling the compiler to
a) grab the first element,
b) create one if not present, and then assign that to the value you requested.
See official api doc specifically mentioning positive index can grow size, negative index past the beginning of the array raises an error.
Q2:
The above explanation almost answers the second question as well.
Given a = [1,2]
a[-3] = 3 causes the first point of break. You are trying to access the 3rd element from the end, which does not exist. By design it breaks down.
While a[-2..-3] is within the capture range of the defined array.
You ask the interpreter to capture the second element from the last (1st from the front in this case), and try to invoke a range which is asking it to increase the array's size, and populate it with whatever you requested.
Happily, all is still well and as desired. Good to know.
Observation #1
The -1 index is related to the last element, if the array has no size, [], you can't use it until you initialize it with one or more elements.
Observation #2:
Yes, you are right, the negative index never increases size of the array, it only references a concrete existing position in the array.
Don't think the array is circular—the 0 index clued to the N-1 index—so you can't use any negative index thinking that it's valid.
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Array slicing in Ruby: looking for explanation for illogical behaviour (taken from Rubykoans.com)
I have a simple array object:
array = [:peanut, :butter, :and, :jelly]
Its length is, clearly, 4!
Now, take a look at these statements:
array[4,0] == []
array[5,0] == nil
Slicing the array from 4th index, it returns an empty array, but starting from 5th element, it returns nil. Why this happens?
slice(start, length) (which is the method called when you access array[]) returns an array of length length starting at index start. This explains why array[4,0] return [].
However, there is a corner case in this method. The Ruby documentation of slice says:
Returns nil if the index (or starting index) are out of range.
In your second example, 5 is out of range for array. This causes the method to return nil.
For a second, I thought you had found a bug, but I think I can explain this behaviour. The starting index you give to slice is considered just before the element at that index. So index 4 is considered the very end of the array (just before index 4, just after index 3). The following examples should make it clear:
> [1,2,3,4][0, 3]
# => [1, 2, 3]
> [1,2,3,4][1, 3]
# => [2, 3, 4]
> [1,2,3,4][2, 3]
# => [3, 4]
> [1,2,3,4][3, 3]
# => [4]
> [1,2,3,4][4, 3]
# => []
> [1,2,3,4][5, 3]
# => nil
Actually, the reason for things you found "strange" should always lie in source code. I just pick the relevant snippet from https://github.com/ruby/ruby/blob/trunk/range.c
...
if (beg < 0) {
beg += len;
if (beg < 0)
goto out_of_range;
}
if (err == 0 || err == 2) {
if (beg > len)
goto out_of_range;
if (end > len)
end = len;
}
...
Here, if starting index beg is smaller than -length, it returns out of range Qnil. Otherwise, if starting index is larger than length of array (len in code snippet), it returns out of range (Qnil) too. NOTICE here, it's larger than not larger or equal than, that's why you get [] for array[4,0] (although 4 is out of range) but nil for array[5,0]
I was going through the exercises in Ruby Koans and I was struck by the following Ruby quirk that I found really unexplainable:
array = [:peanut, :butter, :and, :jelly]
array[0] #=> :peanut #OK!
array[0,1] #=> [:peanut] #OK!
array[0,2] #=> [:peanut, :butter] #OK!
array[0,0] #=> [] #OK!
array[2] #=> :and #OK!
array[2,2] #=> [:and, :jelly] #OK!
array[2,20] #=> [:and, :jelly] #OK!
array[4] #=> nil #OK!
array[4,0] #=> [] #HUH?? Why's that?
array[4,100] #=> [] #Still HUH, but consistent with previous one
array[5] #=> nil #consistent with array[4] #=> nil
array[5,0] #=> nil #WOW. Now I don't understand anything anymore...
So why is array[5,0] not equal to array[4,0]? Is there any reason why array slicing behaves this weird when you start at the (length+1)th position??
Slicing and indexing are two different operations, and inferring the behaviour of one from the other is where your problem lies.
The first argument in slice identifies not the element but the places between elements, defining spans (and not elements themselves):
:peanut :butter :and :jelly
0 1 2 3 4
4 is still within the array, just barely; if you request 0 elements, you get the empty end of the array. But there is no index 5, so you can't slice from there.
When you do index (like array[4]), you are pointing at elements themselves, so the indices only go from 0 to 3.
this has to do with the fact that slice returns an array, relevant source documentation from Array#slice:
* call-seq:
* array[index] -> obj or nil
* array[start, length] -> an_array or nil
* array[range] -> an_array or nil
* array.slice(index) -> obj or nil
* array.slice(start, length) -> an_array or nil
* array.slice(range) -> an_array or nil
which suggests to me that if you give the start that is out of bounds, it will return nil, thus in your example array[4,0] asks for the 4th element that exists, but asks to return an array of zero elements. While array[5,0] asks for an index out of bounds so it returns nil. This perhaps makes more sense if you remember that the slice method is returning a new array, not altering the original data structure.
EDIT:
After reviewing the comments I decided to edit this answer. Slice calls the following code snippet when the arg value is two:
if (argc == 2) {
if (SYMBOL_P(argv[0])) {
rb_raise(rb_eTypeError, "Symbol as array index");
}
beg = NUM2LONG(argv[0]);
len = NUM2LONG(argv[1]);
if (beg < 0) {
beg += RARRAY(ary)->len;
}
return rb_ary_subseq(ary, beg, len);
}
if you look in the array.c class where the rb_ary_subseq method is defined, you see that it is returning nil if the length is out of bounds, not the index:
if (beg > RARRAY_LEN(ary)) return Qnil;
In this case this is what is happening when 4 is passed in, it checks that there are 4 elements and thus does not trigger the nil return. It then goes on and returns an empty array if the second arg is set to zero. while if 5 is passed in, there are not 5 elements in the array, so it returns nil before the zero arg is evaluated. code here at line 944.
I believe this to be a bug, or at least unpredictable and not the 'Principle of Least Surprise'. When I get a few minutes I will a least submit a failing test patch to ruby core.
At least note that the behavior is consistent. From 5 on up everything acts the same; the weirdness only occurs at [4,N].
Maybe this pattern helps, or maybe I'm just tired and it doesn't help at all.
array[0,4] => [:peanut, :butter, :and, :jelly]
array[1,3] => [:butter, :and, :jelly]
array[2,2] => [:and, :jelly]
array[3,1] => [:jelly]
array[4,0] => []
At [4,0], we catch the end of the array. I'd actually find it rather odd, as far as beauty in patterns go, if the last one returned nil. Because of a context like this, 4 is an acceptable option for the first parameter so that the empty array can be returned. Once we hit 5 and up, though, the method likely exits immediately by nature of being totally and completely out of bounds.
This makes sense when you consider than an array slice can be a valid lvalue, not just an rvalue:
array = [:peanut, :butter, :and, :jelly]
# replace 0 elements starting at index 5 (insert at end or array):
array[4,0] = [:sandwich]
# replace 0 elements starting at index 0 (insert at head of array):
array[0,0] = [:make, :me, :a]
# array is [:make, :me, :a, :peanut, :butter, :and, :jelly, :sandwich]
# this is just like replacing existing elements:
array[3, 4] = [:grilled, :cheese]
# array is [:make, :me, :a, :grilled, :cheese, :sandwich]
This wouldn't be possible if array[4,0] returned nil instead of []. However, array[5,0] returns nil because it's out of bounds (inserting after the 4th element of a 4-element array is meaningful, but inserting after the 5th element of a 4 element array is not).
Read the slice syntax array[x,y] as "starting after x elements in array, select up to y elements". This is only meaningful if array has at least x elements.
This does make sense
You need to be able to assign to those slices, so they are defined in such a way that the beginning and the end of the string have working zero-length expressions.
array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
I found explanation by Gary Wright very helpful as well.
http://www.ruby-forum.com/topic/1393096#990065
The answer by Gary Wright is -
http://www.ruby-doc.org/core/classes/Array.html
The docs certainly could be more clear but the actual behavior is
self-consistent and useful.
Note: I'm assuming 1.9.X version of String.
It helps to consider the numbering in the following way:
-4 -3 -2 -1 <-- numbering for single argument indexing
0 1 2 3
+---+---+---+---+
| a | b | c | d |
+---+---+---+---+
0 1 2 3 4 <-- numbering for two argument indexing or start of range
-4 -3 -2 -1
The common (and understandable) mistake is too assume that the semantics
of the single argument index are the same as the semantics of the
first argument in the two argument scenario (or range). They are not
the same thing in practice and the documentation doesn't reflect this.
The error though is definitely in the documentation and not in the
implementation:
single argument: the index represents a single character position
within the string. The result is either the single character string
found at the index or nil because there is no character at the given
index.
s = ""
s[0] # nil because no character at that position
s = "abcd"
s[0] # "a"
s[-4] # "a"
s[-5] # nil, no characters before the first one
two integer arguments: the arguments identify a portion of the string to
extract or to replace. In particular, zero-width portions of the string
can also be identified so that text can be inserted before or after
existing characters including at the front or end of the string. In this
case, the first argument does not identify a character position but
instead identifies the space between characters as shown in the diagram
above. The second argument is the length, which can be 0.
s = "abcd" # each example below assumes s is reset to "abcd"
To insert text before 'a': s[0,0] = "X" # "Xabcd"
To insert text after 'd': s[4,0] = "Z" # "abcdZ"
To replace first two characters: s[0,2] = "AB" # "ABcd"
To replace last two characters: s[-2,2] = "CD" # "abCD"
To replace middle two characters: s[1..3] = "XX" # "aXXd"
The behavior of a range is pretty interesting. The starting point is the
same as the first argument when two arguments are provided (as described
above) but the end point of the range can be the 'character position' as
with single indexing or the "edge position" as with two integer
arguments. The difference is determined by whether the double-dot range
or triple-dot range is used:
s = "abcd"
s[1..1] # "b"
s[1..1] = "X" # "aXcd"
s[1...1] # ""
s[1...1] = "X" # "aXbcd", the range specifies a zero-width portion of
the string
s[1..3] # "bcd"
s[1..3] = "X" # "aX", positions 1, 2, and 3 are replaced.
s[1...3] # "bc"
s[1...3] = "X" # "aXd", positions 1, 2, but not quite 3 are replaced.
If you go back through these examples and insist and using the single
index semantics for the double or range indexing examples you'll just
get confused. You've got to use the alternate numbering I show in the
ascii diagram to model the actual behavior.
I agree that this seems like strange behavior, but even the official documentation on Array#slice demonstrates the same behavior as in your example, in the "special cases" below:
a = [ "a", "b", "c", "d", "e" ]
a[2] + a[0] + a[1] #=> "cab"
a[6] #=> nil
a[1, 2] #=> [ "b", "c" ]
a[1..3] #=> [ "b", "c", "d" ]
a[4..7] #=> [ "e" ]
a[6..10] #=> nil
a[-3, 3] #=> [ "c", "d", "e" ]
# special cases
a[5] #=> nil
a[5, 1] #=> []
a[5..10] #=> []
Unfortunately, even their description of Array#slice doesn't seem to offer any insight as to why it works this way:
Element Reference—Returns the element at index, or returns a subarray starting at start and continuing for length elements, or returns a subarray specified by range. Negative indices count backward from the end of the array (-1 is the last element). Returns nil if the index (or starting index) are out of range.
An explanation provided by Jim Weirich
One way to think about it is that index position 4 is at the very edge
of the array. When asking for a slice, you return as much of the
array that is left. So consider the array[2,10], array[3,10] and
array[4,10] ... each returns the remaining bits of the end of the
array: 2 elements, 1 element and 0 elements respectively. However,
position 5 is clearly outside the array and not at the edge, so
array[5,10] returns nil.
Consider the following array:
>> array=["a","b","c"]
=> ["a", "b", "c"]
You can insert an item to the begining (head) of the array by assigning it to a[0,0]. To put the element between "a" and "b", use a[1,0]. Basically, in the notation a[i,n], i represents an index and n a number of elements. When n=0, it defines a position between the elements of the array.
Now if you think about the end of the array, how can you append an item to its end using the notation described above? Simple, assign the value to a[3,0]. This is the tail of the array.
So, if you try to access the element at a[3,0], you will get []. In this case you are still in the range of the array. But if you try to access a[4,0], you'll get nil as return value, since you're not within the range of the array anymore.
Read more about it at http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .
tl;dr: in the source code in array.c, different functions are called depending on whether you pass 1 or 2 arguments in to Array#slice resulting in the unexpected return values.
(First off, I'd like to point out that I don't code in C, but have been using Ruby for years. So if you're not familiar with C, but you take a few minutes to familiarize yourself with the basics of functions and variables it's really not that hard to follow the Ruby source code, as demonstrated below. This answer is based on Ruby v2.3, but is more or less the same back to v1.9.)
Scenario #1
array.length == 4; array.slice(4) #=> nil
If you look at the source code for Array#slice (rb_ary_aref), you see that when only one argument is passed in (lines 1277-1289), rb_ary_entry is called, passing in the index value (which can be positive or negative).
rb_ary_entry then calculates the position of the requested element from the beginning of the array (in other words, if a negative index is passed in, it computes the positive equivalent) and then calls rb_ary_elt to get the requested element.
As expected, rb_ary_elt returns nil when the length of the array len is less than or equal to the index (here called offset).
1189: if (offset < 0 || len <= offset) {
1190: return Qnil;
1191: }
Scenario #2
array.length == 4; array.slice(4, 0) #=> []
However when 2 arguments are passed in (i.e. the starting index beg, and length of the slice len), rb_ary_subseq is called.
In rb_ary_subseq, if the starting index beg is greater than the array length alen, nil is returned:
1208: long alen = RARRAY_LEN(ary);
1209:
1210: if (beg > alen) return Qnil;
Otherwise the length of the resulting slice len is calculated, and if it's determined to be zero, an empty array is returned:
1213: if (alen < len || alen < beg + len) {
1214: len = alen - beg;
1215: }
1216: klass = rb_obj_class(ary);
1217: if (len == 0) return ary_new(klass, 0);
So since the starting index of 4 is not greater than array.length, an empty array is returned instead of the nil value that one might expect.
Question answered?
If the actual question here isn't "What code causes this to happen?", but rather, "Why did Matz do it this way?", well you'll just have to buy him a cup of coffee at the next RubyConf and ask him.