1

How can I rewrite the following code to be more Ruby-wayish? I'm thinking about inject but can't figure out how to do it.

  def nested_page_path(page)
    path = "/#{page.slug}"
    while page.parent_id do
      path.prepend "/#{page.parent.slug}"
      page = page.parent
    end
    path
  end

Input is an AR object, that has 0-5 consecutive parents. And output is something like '/pages/services/law'.

2
  • 2
    What is the input, and expected output of the method? Commented Jun 5, 2015 at 11:01
  • 0-5 consecutive parents would make 0-5 consecutive queries. What data structure are you using? Nested sets, for instance, would need to make only 1. Commented Jun 5, 2015 at 13:09

3 Answers 3

2

If you know for sure that there are no cycles in your parenting, you can do that recursively, i. e. with a function that calls itself. 5-level nesting should do just fine, trouble could arise with thousands.

def nested_page_path(page)
  return "" if page.nil? # Or whatever that is root
  "#{nested_page_path(page.parent)}/#{page.slug}"
end

But bear in mind, that the approach above, as well as yours, will fetch each object in a separate query. It's fine when you already have them fetched, but if not, you're in a bit of N+1 query trouble.

An easy workaround is caching. You can rebuild the nested path of this object and its descendants on before_save: that is some significant overhead on each write. There is a much better way.

By using nested sets you can get the object's hierarchy branch in just one query. Like this:

page.self_and_ancestors.pluck(:slug).join('/')
#             ^
#   Nested sets' goodness

What that query does is essentially "fetch me pages ordered by left bound, ranges of which enclose my own". I'm using awesome_nested_set in my examples.

SELECT "pages"."slug" FROM "pages"
WHERE ("pages"."lft" <= 42) AND ("pages"."rgt" >= 88)
ORDER BY "pages"."lft"
Sign up to request clarification or add additional context in comments.

Comments

1

Without knowing your object structure it's difficult. But something recursive like this should do:

def nested_page_path(page)
  path = "/#{page.slug}"
  return path unless page.parent_id
  path.prepend "#{nested_page_path(page.parent)}/"
end

Comments

1

Not sure inject is the simple answer since it operates on an Enumerable and you don’t have an obvious enumerable to start with.

I’d suggest something like this (not unlike your solution)

def nested_page_path(page)
  pages = [page]
  pages << pages.last.parent while pages.last.parent
  '/' + pages.reverse.map(&:slug).join('/')
end

There’s scope for reducing repetition there, but that’s more or less what I’d go with.

1 Comment

Mostly to satisfy my own curiosity, I went for a solution involving inject. It's much more cumbersome due to the need to create an Enumerable class first. Solution is here

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.