belgoros
belgoros

Reputation: 3908

How to assign a value to a variable depending on array processing result

Is there is a better solution for such a trivial task?

Given an array of strings as follows:

roles = [
    "id=Accountant,id=TOTO,id=client",
    "id=Admin,id=YOYO,id=custom",
    "id=CDI,id=SC"
    ]

To extract a role value based on its id value I'm using the following regex expression to match it:

r =~ /id=Admin/

The dumb simple solution would be just to iterate on the roles array, assign the matched value and return it as follows:

role = nil
roles.each do |r|
 role = 'admin' if r =~ /id=Admin/
 role = 'national' if r =~ /id=National/
role = 'local' if r =~ /id=Local/
end

role

Is there a better solution?

Upvotes: 1

Views: 84

Answers (4)

Kimmo Lehto
Kimmo Lehto

Reputation: 6041

Well the obvious way I think would be to simply parse the whole roles array:

roles = [
  "id=Accountant,id=TOTO,id=client",
  "id=Admin,id=YOYO,id=custom",
  "id=CDI,id=SC"
]

user_roles = roles.join(',').split(',').map { |r| r.split('=', 2).last.downcase }

Where user_roles becomes:

["accountant", "toto", "client", "admin", "yoyo", "custom", "cdi", "sc"]

Now you can simply do something like:

user_roles.include?('admin')

Or to find any of the "admin", "national", "local" occurences:

# array1 AND array2, finds the elements that occur in both:
> %w(admin national local) & user_roles
=> ["admin"]

Or perhaps to just find out if the user has any of those roles:

# When there are no matching elements, it will return an empty array
> (%w(admin national local) & user_roles).empty?
=> false 
> (["megaboss", "superadmin"] & user_roles).empty?
=> true 

And here it is in a more complete example with constants and methods and all!

SUPERVISOR_ROLES = %w(admin national local)

def is_supervisor?(roles)
  !(SUPERVISOR_ROLES & roles).empty?
end

def parse_roles(raw_array)
  raw_array.flat_map { |r| r.split(',').map { |r| r.split('=', 2).last.downcase } }
end

roles = [
   "id=Accountant,id=TOTO,id=client",
   "id=Admin,id=YOYO,id=custom",
   "id=CDI,id=SC"
 ]

raise "you no boss :(" unless is_supervisor?(parse_roles(roles))

This of course may be inefficient if the data set is large, but a bit cleaner and maybe even safer than performing such a regex, for example someone could create a role called AdminHack which would still be matched by the /id=Admin/ regex and by writing such a general role parser may become useful along the way if you want to check for other roles for other purposes.

(And yes, obviously this solution creates a hefty amount of intermediary arrays and other insta-discarded objects and has plenty of room for optimization)

Upvotes: 2

Stefan
Stefan

Reputation: 114138

You could define a regular expression to match several roles at once. Here's a simple one:

/id=(Admin|National|Local)/

The parentheses act as a capturing group for the role name. You might want to add anchors, e.g. to only match the first id=value pair in each line. Or to ensure that you match the whole value instead of just the beginning if these can be ambiguous.

The pattern can then be passed to grep which returns the matching lines:

roles.grep(/id=(Admin|National|Local)/)
#=> ["id=Admin,id=YOYO,id=custom"]

Passing a block to grep allows us to transform the match: ($1 refers to the first capture group)

roles.grep(/id=(Admin|National|Local)/) { $1.downcase }
#=> ["admin"]

To get the first role:

roles.grep(/id=(Admin|National|Local)/) { $1.downcase }.first
#=> "admin"

If your array is large you can use a lazy enumerator which will stop traversing after the first match:

roles.lazy.grep(/id=(Admin|National|Local)/) { $1.downcase }.first
#=> "admin"

Upvotes: 3

the Tin Man
the Tin Man

Reputation: 160551

I like Stefen's answer, but didn't like that it could run a long time grabbing id values before exiting the grep if the list of roles was really big. I also didn't like the pattern because it wasn't anchored to the beginning of the search string, forcing the engine to do more work.

I'd rather see the code stop at the first hit so this was a first attempt:

roles = [
  "id=Accountant,id=TOTO,id=client",
  "id=Admin,id=YOYO,id=custom",
  "id=CDI,id=SC"
]

found_role = nil
roles.each do |i|
  r = i[/^id=(Admin|National|Local)/]
  if r
    found_role = r.downcase 
    break
  end
end
found_role # => "id=admin"

Thinking about that kept nagging at me as being too verbose, so this popped out:

roles = [
  "id=Accountant,id=TOTO,id=client",
  "id=Admin,id=YOYO,id=custom",
  "id=CDI,id=SC"
]

roles.find { |i| i[/^id=(Admin|National|Local)/] }.downcase[/^(id=\w+),/, 1]
# => "id=admin"

Breaking it down, here are the high spots:

  • i[/^id=(Admin|National|Local)/] returns the matching string "id=Admin..." and exits the loop.
  • downcase[/^(id=\w+),/, 1] grabs the first pair and returns it.

Then, being as anal as I am, I figured downcase would be doing too much work too so this happened:

roles.find { |i| i[/^id=(Admin|National|Local)/] }[/^(id=\w+),/, 1].downcase

It's pretty cryptic Ruby, and we're not really supposed to write code this way, but I used to write C and Perl so it seems reasonable to me.

And the interesting part:

require 'fruity'

roles = [
  "id=Accountant,id=TOTO,id=client",
  "id=Admin,id=YOYO,id=custom",
  "id=CDI,id=SC"
]

compare do
  numero_uno {
    found_role = nil
    roles.each do |i|
      r = i[/^id=(Admin|National|Local)/]
      if r
        found_role = r.downcase 
        break
      end
    end
    found_role
  }

  numero_dos { roles.find { |i| i[/^id=(Admin|National|Local)/] }.downcase[/^(id=\w+),/, 1] }

  numero_tres { roles.find { |i| i[/^id=(Admin|National|Local)/] }[/^(id=\w+),/, 1].downcase }
end

# >> Running each test 2048 times. Test will take about 1 second.
# >> numero_uno is similar to numero_tres
# >> numero_tres is faster than numero_dos by 10.000000000000009% ± 10.0%

Upvotes: 1

Cary Swoveland
Cary Swoveland

Reputation: 110675

If we wish to find a particular spy from a group of cells we could merely round up all the spies from all the cells and examine them sequentially until the culpit is found.

Here the equivalent is to join the strings from the given array to form a single string and then search that string for the given substring:

str = roles.join(' ').downcase
  #=> "id=accountant,id=toto,id=client id=admin,id=yoyo,id=custom id=cdi,id=sc"

join's argument could be a space, newline, comma or any of several other strings (I've used a space).

We then simply look for a match, using the method String#[] and the regular expression:

r = /
    (?<=id=)                 # match 'id=' in a positive lookbehind
    (?:admin|national|local) # match 'admin', 'national' or 'local'
    (?!\w)                   # do not match a word character (negative lookahead)
    /x                       # free-spacing regex definition mode

In normal (not free-spacing mode) this is:

/(?<=id=)(?:admin|national|local)(?!\w)/

'id=', being in a positive lookbehind, is not included in the match. The negative lookahead, (?!\w), ensures that the match is not immediately followed by a word character. That prevents a match, for example, on the word 'administration'.

We now simply extract the match, if there is one:

str[r] #=> "admin"

Had there not been a match, nil would have been returned.

We could have instead downcased at the end:

str = roles.join(' ')
str[/(?<=id=)([aA]dmin|[nN]ational|[lL]ocal)(?!\w)/i].downcase

Upvotes: 1

Related Questions