Reputation: 3908
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
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
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
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
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