Extract symbols from an Array that has been converted to a String?

147 Views Asked by At

I have the following string:

s = "[:test1, :test2]"

I want it to end up as an Array of symbols again:

[:test1, :test2]

I found two similar questions:

Ruby: Parsing a string representation of nested arrays into an Array?

Ruby convert string array to array object

But neither the JSON.parse or YAML.load work:

irb(main):043> test
=> "[:test1, :test2]"
irb(main):044> JSON.parse test
/Users/[...]/.rbenv/versions/3.2.2/lib/ruby/3.2.0/json/common.rb:216:in `parse':
unexpected token at ':test1, :test2]' (JSON::ParserError)
irb(main):045>
irb(main):045> YAML.load test
/Users/[...]/.rbenv/versions/3.2.2/gemsets/rails_test/gems/psych-5.1.1.1/lib/psych/parser.rb:62:in `_native_parse': (<unknown>):
did not find expected node content while parsing a flow node at line 1 column 2 (Psych::SyntaxError)

eval does work but is generally not recommended due to injection concerns.

irb(main):047> eval(test)
=> [:test1, :test2]

The only way I can find to do it is some basic string manipulation, but I was wondering if there was a better way built into Ruby that does not involve creating a custom function.

irb(main):046> test.delete("[]:").split(", ").map(&:to_sym)
=> [:test1, :test2]

Ruby 3.2.2 for reference.

2

There are 2 best solutions below

1
Todd A. Jacobs On BEST ANSWER

You can clean up your example, but it's not guaranteed to work on anything that's not a flat array of symbols stored inside a string.

s = "[:test1, :test2]"

s = s.delete(":[]").split(?,).map { _1.strip.to_sym }
=> [:test1, :test2]

However, it's entirely unclear how you're ending up with a String instead of an Array of Symbols, and nothing in the Rails Guide about composite keys explains how you're ending up in that state. I'm forced to assume you're getting data from form input or URI parameters (both of which are represented as strings) and not parsing the elements as separate column names so you can properly form the composite key from the two String values.

The section on composite keys in forms says that the keys must be delimited by an underscore for the Rails magic to work, and you may also need to implement params.extract_value in your controller to access them properly. You didn't show any controller, view, or model code, so it seems like you're trying to patch up the improperly-formatted end result instead of focusing on the source of the incorrect data.

Even if that's not the case, this is ultimately solving the wrong problem. You need to figure out why s contains invalid JSON, or why you aren't getting two separate keys as String objects (not Symbols) that you can then use to form your composite key properly.

Solve for those things. The post facto String munging is addressing the symptom, not the cause.

1
Cary Swoveland On

Rather than remove bits that you don't want I suggest you extract the strings you do want (then covert them to symbols).

s = "[:test1, :test2]"
s.scan(/\w+/).map(&:to_sym)
  #=> [:test1, :test2]

See String#scan and String#to_sym.


If desired one could modify the regular expression slightly to confirm that each colon is present.

s.scan(/:\K\w+/).map(&:to_sym)
  #=> [:test1, :test2]

or

s.scan(/(?<=:)\w+/).map(&:to_sym)
  #=> [:test1, :test2]

Here \K resets the start of the match to the current location in the string and discards any previously consumed characters (":"). (?<=:) is a positive lookbehind that asserts that the beginning of the match is immediately preceded by a colon.

These refinements are of dubious value, however. If, for example, the string were "[test1, :test2]", [:test2] would be returned, whereas raising an ArgumentError exception might be more appropriate. If the correctness of the string format were in question it would probably be more useful to perform a separate check by executing the following.

s.match?(/\A *\[ *:\w+(?: *, *:\w+)* *\] *\z/)

Demo1

This regular expression can be expressed in free-spacing mode to make it self-documenting.

/
\A          # match the beginning of the string
\ *\[\ *    # match >= 0 spaces followed by '[', followed by >= 0 spaces 
:\w+        # match ':' followed by >= 1 word characters
(?:         # begin a non-capture group
   \ *,\ *  # match >= 0 spaces followed by ',', followed by >= 0 spaces
   :\w+     # match ':' followed by >= 1 word characters
)*          # end the non-capture group and execute it >= 0 times
\ *\]\ *    # match >= 0 spaces followed by '', followed by >= 0 spaces
\z          # match the end of the string
/x          # invoke free-spacing mode 

In free-spacing mode it is necessary to protect any spaces that are part of the expression. I've done that by escaping spaces; putting each space in a character class ([ ]) is another common way to do that.

1. At the link I've used start-of-line (^) and end-of-line ($) anchors rather than start-of-string (\A) and end-of-string (\z) anchors in order to test multiple strings.