Unexpected behavior of Ruby Array? [duplicate] - ruby

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.

Related

Ruby - How to write a method that returns an array of strings?

I've tried different ways and this is probably the closest that I got to it. I am trying to write a method that takes in an array of strings and returns it containing the strings that are at least 5 characters long and end with "y".
I'm a beginner and this is my second problem I've come across with, and I've tried multiple if statements and using a while loop, however I could not get to it and now this is where I am at. Thank you!
def phrases(arr1, arr2)
arr1 = ["funny", "tidy", "fish", "foogiliously"]
arr2 = ["happily", "lovely", "hello", "multivitaminly"]
if (arr1.length > 5 && arr1.length == "y")
return arr1
elsif (arr2.length > 5 && arr2.length == "y")
return arr2
end
end
puts phrases(["funny", "tidy", "fish", "foogiliously"])
puts phrases(["happily", "lovely", "hello", "multivitaminly"])
If I'm understanding your question correctly, you want to return a subset of the passed in array matching your conditions (length ≥ 5 and last character = 'y'). In that case:
def phrases(words)
words.grep(/.{4}y\z/)
end
What that regex does:
.{4} means 4 of any character
y is the letter y
\z is the end of the string, so we don't match in the middle of a long word
The docs for Enumerable#select are here (an Array is an Enumerable).
Output:
> phrases(["funny", "tidy", "fish", "foogiliously"])
=> ["funny", "foogiliously"]
> phrases(["happily", "lovely", "hello", "multivitaminly"])
=> ["happily", "lovely", "multivitaminly"]
If you only want word characters, rather than any character, you'd use this regex instead: /\A.{4,}y\z/. In that case, \A means the start of the string, and \w{4,} means at least 4 word characters.
If, when given an array and inclusion criterion, one wishes to construct an array that contains those elements of the first array that satisfy the inclusion criterion, one generally uses the method Array#select or Array#reject, whichever is more more convenient.
Suppose arr is a variable that holds the given array and include_element? is a method that takes one argument, an element of arr, and returns true or false, depending on whether the inclusion criterion is satisified for that element. For example, say the array comprises the integers 1 through 6 and the inclusion criterion is that the number is even (2, 4 and 6). We could write:
arr = [1,2,3,4,5,6]
def include_element?(e)
e.even?
end
include_element?(2)
#=> true
include_element?(3)
#=> false
arr.select { |e| include_element?(e) }
#=> [2, 4, 6]
The method include_element? is so short we probably would substitute it out and just write:
arr.select { |e| e.even? }
Array#select passes each element of its receiver, arr, to select's block, assigns the block variable e to that value and evaluates the expression in the block (which could be many lines, of course). Here that expresssion is just e.even?, which returns true or false. (See Integer#even? and Integer#odd?.)
If that expression evaluates as a truthy value, the element e is to be included in the array that is returned; if it evaluates as a falsy value, e is not to be included. Falsy values (logical false) are nil and false; truthy values (logical true) are all other Ruby objects, which of course includes true.
Notice that we could instead write:
arr.reject { |e| e.odd? }
Sometimes the inclusion criterion consists of a compound expression. For example, suppose the inclusion criterion were to keep elements of arr that are both even numbers and are at least 4. We would write:
arr.select { |e| e.even? && e >= 4 }
#=> [4, 6]
With other criteria we might write:
arr.select { |e| e.even? || e >= 4 }
#=> [2, 4, 5, 6]
or
arr.select { |e| e < 2 || (e > 3 && e < 6) }
#=> [1, 4, 5]
&& (logical 'and') and || (logical 'or') are operators (search "operator expressions"). As explained at the link, most Ruby operators are actually methods, but these two are among a few that are not.
Your problem now reduces to the following:
arr.select { |str| <length of str is at least 5> && <last character of str is 'y'> }
You should be able to supply code for the <...> bits.
You are trying to write a function that should work on a single array at a time I think. Also, you are taking in an array, and retaining only those elements that satisfy your conditions: at least 5 characters long, and ends with y. This is a filtering operation. Read about the methods available for ruby's Array class here
def phrases(array)
...
filtered_array
end
Now the condition you are using is this arr1.length > 5 && arr1.length == "y".
The first half should check if the string length is greater than 5, not the array length itself. The second half is an indexing operation, and your code for that is incorrect. basically you are checking if the last character in the string is y.
Usually strings are indexed in this manner: string[index]. In your case you can use string[-1]=='y' or string[string.length - 1]=='y'. This because arrays and strings are zero indexed in ruby. The first element has index of 0, the second has an index of 1, and the last one, therefore, will have an index of length-1. If you use negative indexes then the array is indexed from the end, so string[-1] is a quick way to get to the last element.
Considering this, the function will take the following structure:
def phrases(array)
filtered_array = [] # an empty array
loop through the input array
for each element check for the condition element.length > 5 && element[-1]=='y'
if true: push the element into the filtered_array
once the loop is done, return the filtered array
end
Read about ruby arrays, the methods push, filter and select in the above linked documentation to get a better idea. I'd also recommend the codeacademy ruby tutorial.
Edit: Both halves of the condition are incorrect. I had overlooked a mistake in my earlier answer. arr1.length refers to the length of the array. You want to check the length of each string in the array. So in your for loop you should check the length of the loop variable, if that is greater than 5.
You may want to spend some time reading about the methods in the core library, especially String#end_with? and Enumerable#select. You could then write a method that'd contain something like this:
['abc', 'qwerty', 'asdfghjk', 'y'].select{|s| s.length >= 5}.select{|s| s.end_with? 'y'}
#=> ["qwerty"]

Explain behaviour of ruby array when selecting inexistent elements [duplicate]

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.

Why is array[4,43] == [] for an array of size 4 in Ruby?

a = [1,2,3,4]
a[4,43]
# => []
a[5,43]
# => nil
What is the reason for this anomaly?
Assuming you mean this:
a = [1,2,3,4,5]
a[5,43]
# => []
a[6,43]
# => nil
From http://bugs.ruby-lang.org/issues/4245:
This is per spec.
Think of it this way:
[5..-1]: give me all elements after the 5th one and up to the last
one. There are none, so []
[6..-1]: give me all elements after the 6th
one and up to the last one. Say what? There is no 6th one, so returns
nil as the given request is out of bounds.
I agree with spartacus and the bug reporter from the link by Dylan -- there are indeed inconsistencies and Array#slice could use a change. Although yes, it is the spec; from Array#slice
For start and range cases the starting index is just before an element. Additionally, an empty array is returned when the starting index for an element range is at the end of the array ... Returns nil if the index (or starting index) are out of range.
[ 0, 1, 2, 3 ]
^ ^ ^ ^ ^
0 1 2 3 4
Let's say the array a was constructed as [0,1,2,3] , and the numbers below the hats represent where one would "start" when using the a[start,length] form. So a[0,0] is [], and a[1,0] is [], since we have not "hopped" anywhere. Similarly, a[4,0] is [], and that's fine. Now a[4,x] where x is any nonzero number, should return nil, in my opinion, since a[4] is undefined, and in theory, a[4,x] is asking for an array that would look like [undefined, undefined...]
For ranges, however, it appears that an array is built from all indices that appear in the range, so the "hat" start visualization does not apply. So a[0..2] returns [a[0],a1,a[2]], or [0,1,2] in this case.
a[4..x] should return nil for any x, since there is no a[4].

How does negative index work with `Array#[]=`?

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.

Ruby arrays slice, can't understand this simple code snippet [duplicate]

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]

Resources