DEV Community

Cover image for Arrays: linear containers in Ruby
Dandy Vica
Dandy Vica

Posted on • Updated on

Arrays: linear containers in Ruby

In this article, I'll focus on Ruby. I'll use the version 2.5.1:

ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux-gnu]
Enter fullscreen mode Exit fullscreen mode

Basic Ruby arrays

Like my previous article on Python here Python lists, I'll focus on some more advanced array features rather than basic ones.

digits = [0,1,2,3,4,5,6,7,8,9] 
a = ["one", "two", "three", "four"]
Enter fullscreen mode Exit fullscreen mode

Creating an array in Ruby is as easy as in Python:

digits = [0,1,2,3,4,5,6,7,8,9] 
a = ["one", "two", "three", "four"]

# empty araay
empty = []
empty = Array.new()
empty = Array.new
Enter fullscreen mode Exit fullscreen mode

You can mix types:

mixed_array = ["one", 2, "three", 4]
Enter fullscreen mode Exit fullscreen mode

When dealing with strings, you can use the %w() construct to build an array from a litteral string:

lipsum = %w(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.)
Enter fullscreen mode Exit fullscreen mode

or even create an array of arrays:

binomial_coefficients = [
    [1],
    [1,1],
    [1,2,1],
    [1,3,3,1],
    [1,4,6,4,1],
    [1,5,10,10,5,1]
]
Enter fullscreen mode Exit fullscreen mode

To initialize an array with the same element, just use * :

['A'] * 5       # gives ['A', 'A', 'A', 'A', 'A']
# or
Array.new(5,'A')
Enter fullscreen mode Exit fullscreen mode

The number of elements of an array is given by length() method:

binomial_coefficients.length            # gives 6
lipsum.length                           # 19
Enter fullscreen mode Exit fullscreen mode

You can also store objects, classes or functions in an array:

# array of functions
trigo = [Math.method(:sin),Math.method(:cos),Math.method(:tan)]

# array of lambdas
powers = [
    ->(x) { x*x }, 
    ->(x) { x**3 },
    ->(x) { x**4 }
]

# array of classes
require 'set'
collections = [Array, Hash, Set]

# array of objects
empty_collections = [Array.new, Hash.new, Set.new]
Enter fullscreen mode Exit fullscreen mode

Accessing elements

Accessing array elements is business as usual, with some very useful refinements :

first_binomial = binomial_coefficients[0]
last_binomial = binomial_coefficients[-1]
fifth_binomial = binomial_coefficients[-2]
binomial_coefficients[4].length # gives 5
Enter fullscreen mode Exit fullscreen mode

Sub-arrays are possible using index slices:

first3_binomials = binomial_coefficients[0...3]
first4_binomials = binomial_coefficients[0..3]
Enter fullscreen mode Exit fullscreen mode

Note the difference between the open range (...) and the closed one (..).

Array operations

  • adding an element
digits.push(10)
Enter fullscreen mode Exit fullscreen mode
  • deleting an element by index
digits.delete_at(10)
Enter fullscreen mode Exit fullscreen mode
  • concatenating arrays
digits = [0,1,2,3,4] + [5,6] + [7,8,9]
Enter fullscreen mode Exit fullscreen mode
  • testing element membership
# methods ending with a trailing ? return a boolean
if digits.include?(9) then
    puts("9 is a digit ! Such a surprise ;-)")
end
Enter fullscreen mode Exit fullscreen mode

Looping through an array

Use the for-in construct (also available for until, while, until)

for d in digits do
    puts(d)
end
Enter fullscreen mode Exit fullscreen mode

but a more functional oriented way is to use the each method:

digits.each { |d| puts(d) }
Enter fullscreen mode Exit fullscreen mode

To get the element index when looping, use the each_with_index method:

digits.each_with_index { |d,i| 
    puts("#{d} is the #{i}-th digit")
}
Enter fullscreen mode Exit fullscreen mode

More advanced usage

Some useful functions on arrays

digits = [0,1,2,3,4,5,6,7,8,9]
digits.sum      # gives 45
digits.max      # gives 9
digits.min      # gives 0

# get any word being the longest
lipsum.max{ |a, b| a.length <=> b.length }           # gives 'consectetur'
Enter fullscreen mode Exit fullscreen mode

The zip() built-in array method combines several arrays to create a resulting array, created by taking the i-th element of each array:

a = [0,1,2,3]
b = [4,5,6,7]
a.zip(b) # gives [[0, 4], [1, 5], [2, 6], [3, 7]]
Enter fullscreen mode Exit fullscreen mode

No array comprehensions

In Ruby, there's no clean syntax as list comprehensions in Python. But those are very akin to higher order function as map. So you can achieve the same result with map or collect methods:

# extract words ending with 't'
# compact() method is used to get rid of nil values
lipsum = %w(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.)
end_with_t = lipsum.map { |w| w if w.end_with?('t') }.compact  # gives ['sit', 'incididunt', 'ut', 'et']

# convert to uppercase
lipsum.map { |w| w.upcase }
lipsum.map(&:upcase)

# get only words of length 5 (including commas)
lipsum.map { |w| w if w.length == 5 }.compact # gives ['Lorem', 'ipsum', 'dolor', 'amet,', 'elit,', 'magna']

# create new objects from an array of classes
require 'set'
[Array, Hash, Set].map{ |cls| cls.new }

# same as before when function called doesn't require an argument
[Array, Hash, Set].map(&:new)

# get pi/4 value from trigonometric functions array
trigo = [Math.method(:sin),Math.method(:cos),Math.method(:tan)]
trigo.map { |f| f.call(Math::PI/4) }
Enter fullscreen mode Exit fullscreen mode

Using the to_a method on an enumerable

Similar to the built-in list() function in Python, Ruby comes with the to_a method which creates an array from an enumerable (Ruby name for iterator):

# this creates an array of a-z chars
a_to_z = ('a'..'z').to_a

# create digits and the first 100 even numbers
digits = (0..9).to_a
even = (0...100).step(2).to_a

# to_a() is idempotent, but this creates another reference not a new array
digits.to_a # gives back a copy of digits 
Enter fullscreen mode Exit fullscreen mode

This obviously works for user defined iterators:

class One
    include Enumerable

    def each
        yield "one"
        yield "un"
        yield "ein"
        yield "uno"
    end
end

# gives ['one', 'un', 'ein', 'uno']
One.new.to_a
Enter fullscreen mode Exit fullscreen mode

Inheriting the Array class

Nothing prevents you to subclass the Array class to create your own user-defined array. This example implements a way to access elements of an array by giving several indexes:

# Based on the Array class, but accept sparse indexes
# Doesn't manage range of chars
class MyArray < Array
    def [](*index)
        if index.length == 1 && 
            (index[0].class == Range || index[0].class == Integer)
            super(index[0])
        else
            index.collect { |i| super(i) }
        end
    end
end

a = MyArray.new(('a'..'z').to_a)
a[1]              # "b"
a[0..3]           # ["a", "b", "c", "d"]
a[0...3]          # ["a", "b", "c"]
a[0,24,25]        # ["a", "y", "z"]
Enter fullscreen mode Exit fullscreen mode

Acting on arrays

Using functional programming built-in functions, you can extract values from an array, or get another array from the source one.

map() or collect()

Using the map() built-in function, it's possible to get an image of a mapping on the array. If you consider an array as a mathematical set of elements, map() gives the image set through the considered function.

a_to_z = ('a'..'z').to_a
A_to_Z = a_to_z.map{ |c| c.upcase }

# map() uses a block which can be more advanced
# greek letters: ["α", "β", "γ", "δ", "ε", "ζ", "η", "θ", "ι", "κ", "λ", "μ", "ν", "ξ", "ο", "π", "ρ", "ς", "σ", "τ", "υ", "φ", "χ", "ψ", "ω", "ϊ", "ϋ", "ό", "ύ", "ώ"]
greek = ('α'..'ω').to_a
greek.map{ |g| 
    case g
        when "α"
            "A"
        when "β"
            "B"
        when "γ"
            "C"
        # and so on
        else
            "X"
    end
}
Enter fullscreen mode Exit fullscreen mode

Of course, the map function to pass as the first argument could be any function, and any lambda having one argument is possible:

digits = (0..9).to_a
digits.map( &->(x) { x*10 } )        # gives tenths

# refer to binomial_coefficients above
binomial_coefficients.map(&:sum)  # gives [1, 2, 4, 8, 16, 32]
Enter fullscreen mode Exit fullscreen mode

or even a user-defined function:

# contrived example
def square(x)
    x**2
end

# calculate first 9 perfect squares
digits = (0..9).to_a
digits.map(&method(:square))      # gives [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Enter fullscreen mode Exit fullscreen mode
  • select()

This built-in function is used to sieve elements from an array, using some criteria. Elements kept are those where the function given as the first argument to select() returns true.

# extract even numbers
digits = (0..9).to_a
digits.select { |n| n%2 == 0 }        # gives [0, 2, 4, 6, 8]

# extract words less than 4 chars
lipsum = %w(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.)
lipsum.select { |w| w.length < 4 }   # gives ['sit', 'sed', 'do', 'ut', 'et']
Enter fullscreen mode Exit fullscreen mode
  • inject()

Refer to my previous article on Python's reduce() to get some details on the inject() method.

Examples:

digits = (0..9).to_a

# sum of first 10 digits
digits.inject { |x,y| x+y }   # gives 45

# this uses an initializer
digits.inject(10) { |x,y| x+y } # gives 55 = 45+10

# a more sophisticated example: this uses the nested multiplication to compute the value of a polynomial, given its coefficients and the unknown value z. BTW, an example of how to extend the Array class ;-)
class Array
    def nested(z)
        self.inject { |x,y| z*x+y }
    end
end
[1,5,10,10,5,1].nested(1)       # gives 32

# easy computation of the nested square root which converges to the golden ratio
([1]*100).inject { |x,y| Math::sqrt(x+y) }   # gives 1.618033988749895

# same for the canonical continued fraction. Note to to_f method to convert to float when dividing, otherwise no division is applied
([1]*100).inject { |x,y| y+1/x.to_f }

# this sums all columns of a matrix
matrix = (0..3).to_a.map { |i| [i]*4 }

matrix.each_with_index.inject([0]*4) { |x, (y,i)|
    x.map!.with_index { |e,i| e+y[i] }
}

Enter fullscreen mode Exit fullscreen mode

Last part will be devoted to Rust vectors.

Photo by Susan Yin on Unsplash

Top comments (0)