0

Can someone help me break down this problem? I have the answer but I'm not really sure how my teacher got to it.

class String
# Write a method, String#substrings, that takes in a optional length argument
# The method should return an array of the substrings that have the given length.
# If no length is given, return all substrings.
#
# Examples:
#
# "cats".substrings     # => ["c", "ca", "cat", "cats", "a", "at", "ats", "t", "ts", "s"]
# "cats".substrings(2)  # => ["ca", "at", "ts"]
 def substrings(length = nil)
    subs = []
    (0...self.length).each do |start_idx|
        (start_idx...self.length) do |end_idx|
            sub = self[start_idx..end_idx]
            subs << sub                
        end
    end

    if length.nil?
      subs
    else
      subs.select { |str| str.length == length}
    end
 end 
end

The start_idx and the end_idx are really confusing, if the start_idx is "ca" for example is the end_idx "ca" as well? Please help..

2
  • start_idx is an index, a number, not a string. Same for end_idx. Commented Nov 19, 2020 at 23:30
  • Im not really getting how it starts at "c" then "ca" then "cat" the "cats" then to "a" Commented Nov 20, 2020 at 0:31

2 Answers 2

2

So think of start_idx and end_idx as a constantly changing variable.

def substrings(length = nil)
    subs = []
    # in the case of 'cats' the length is 4
    # so this is making an array UP TO BUT NOT INCLUDING 4
    # [0,1,2,3].each do ...
    # let's take 0
    (0...self.length).each do |start_idx|
        # start_idx = 0 the first time through this each
        # Array from start_idx UP TO BUT NOT INCLUDING 4
        # so the FIRST time through this is 0, second time through is 1, ...
        #[0,1,2,3].each do ...
        (start_idx...self.length) do |end_idx|
            # end_idx = 0
            # slice of the string from the 0th to the 0th value (first letter)
            sub = self[start_idx..end_idx]
            subs << sub                
        end
    end

    if length.nil?
      subs
    else
      subs.select { |str| str.length == length}
    end
 end

So think of this as a bunch of nested loops using numbers that are reassigned during each pass of the loop.

Does that help?

Sign up to request clarification or add additional context in comments.

1 Comment

kinda, what really confuses me is the second iteration (1). how does it get "ca" from "c" and so on
1

The following would be a more Ruby-like way of writing that.

class String
  def all_substrings
    (1..size).flat_map { |n| all_substrings_by_length(n) }
  end

  def all_substrings_by_length(length)
    each_char.each_cons(length).with_object([]) { |a,arr| arr << a.join }
  end
end

"cats".all_substrings_by_length(1)
  #=> ["c", "a", "t", "s"] 
"cats".all_substrings_by_length(2)
  #=> ["ca", "at", "ts"] 
"cats".all_substrings_by_length(3)
  #=> ["cat", "ats"] 

"cats".all_substrings
  #=> ["c", "a", "t", "s", "ca", "at", "ts", "cat", "ats", "cats"] 

Note that 1..size is the same as 1..self.size, all_substrings_by_length(n) is the same as self.all_substrings_by_length(n) and each_char is the same as self.each_char, as self is implied when a method has no explicit receiver.

See Enumerable#flat_map, String#each_char, Enumerable#each_cons and Emumerator#with_object.


Let's break down

each_char.each_cons(length).with_object([]) { |a,arr| arr << a.join }

when length = 2 and self = "cats".

length = 2
e0 = "cats".each_char
  #=> #<Enumerator: "cats":each_char> 

We can see the elements that will be generated by this enumerator by converting it to an array.

e0.to_a
  #=> ["c", "a", "t", "s"] 

Continuing,

e1 = e0.each_cons(length)
  #=> #<Enumerator: #<Enumerator: "cats":each_char>:each_cons(2)> 
e1.to_a
  #=> [["c", "a"], ["a", "t"], ["t", "s"]] 

e2 = e1.with_object([])
  #=> #<Enumerator: #<Enumerator: #<Enumerator:
  #     "cats":each_char>:each_cons(2)>:with_object([])> 
e2.to_a    
  #=> [[["c", "a"], []], [["a", "t"], []], [["t", "s"], []]] 

By examining the return values for the creation of e1 and e2 one can see that they could be thought of as compound enumerators, though Ruby has no formal concept of such. Also, as will be seen, the empty arrays in the last return value will be built up as the calculations progress.

Lastly,

e2.each { |a,arr| arr << a.join }
  #=> ["ca", "at", "ts"] 

which is our desired result. Now examine this last calculation in more detail. each directs e2 to generate an element and then sets the block variables equal to it.

First, observe the following.

e2
  #=> #<Enumerator: #<Enumerator: #<Enumerator: "cats":each_char>:
  #     each_cons(2)>:=with_object(["ca", "at", "ts"])>

This shows us that we need to return e2 to its initial state in order to reproduce the calculations.

e2 = e1.with_object([])
  #=> #<Enumerator: #<Enumerator: #<Enumerator:
  #     "cats":each_char>:each_cons(2)>:with_object([])> 

Then:

a, arr = e2.next
  #=> [["c", "a"], []] 

Array decomposition breaks this array into parts for a and arr:

a #=> ["c", "a"] 
arr
  #=> [] 

We now perform the block calculation:

arr << a.join
  #=> ["ca"]

each then commands e2 to generate the next element, assigns values to the block variables and performs the block calculation.

a, arr = e2.next
  #=> [["a", "t"], ["ca"]]
a #=> ["a", "t"]
arr
  #=> ["ca"]
arr << a.join
  #=> ["ca", "at"]

This is repeated once more.

a, arr = e2.next
  #=> [["t", "s"], ["ca", "at"]] 
arr << a.join
  #=> ["ca", "at", "ts"] 

Lastly, the exception

a, arr = e2.next
  #=> StopIteration (iteration reached an end)

causes each to return

arr
  #=> ["ca", "at", "ts"]

from the block, which, being the last calculation, is returned by the method.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.