Getting the most out of Ruby strings
Tags: ruby
Published: 2019-10-29 23:14:22

Ruby has a fantastic set of methods on its built-in `String` class, and it's really useful to know how to use them. This will go over some which you might not have heard of!

*Note:* All these methods are from Ruby 2.6.5, and they're all from the standard library; no Active Support required!

----

# `String#+@` and `String#-@`
Strings in Ruby support both the unary plus (`+`) and minus (`-`) operators.

The `+` operator will return a "thaw" a frozen string, returning a mutable copy of it. If the string was already mutable, it has no effect.

```ruby
a = "Hello".freeze
a.downcase! # Throws: FrozenError (can't modify frozen String)

# Now, let's thaw the string first...
a = +a
a.downcase!
a # => "hello"
```

The `-` operator does the opposite, returning a frozen copy of the string. Note that, if a frozen instance of that particular string already exists in Ruby's object pool, it'll return that instance instead.

```ruby
a = "Hello"
a.downcase! # This is fine - our string isn't frozen yet
a = -a
a.upcase! # Throws: FrozenError (can't modify frozen String)
```

You can even combine these two operators for a terse (but horrible) string duplication operator. (Please use `#dup` over this, though!)

```ruby
a = "Hello"
b = +-a
a.gsub!('e', 'a')
a # => "Hallo"
b # => "Hello"
```

# The many uses of `String#[]` 
The square-braces indexing syntax on strings has a gigantic range of use cases in Ruby. Here are the more typical ones you've probably seen before, which involve passing in numbers or ranges.

```ruby
a = "Hello"
a[0]    # Get the character at index 0             => "H"
a[2, 3] # Get 3 characters, starting from index 2  => "llo"
a[1..3] # Get the characters between index 1 and 3 => "ell"
```

Did you know that you can also pass in a regex to get the part of the string which matches it? If there are multiple matches, this will always return the first match.

```ruby
"foo bar baz"[/b../] # => "bar"
```

You can even use capture groups in your regex, and pass an index to specify which capture group to return:

```ruby
"foo bar baz"[/b(..)/, 1] # => "ar"
```

Finally, you can pass a string as an index and it will act like `include?`, except returning the original search string if it was found and `nil` if it was not found. 

```ruby
s = "foo bar baz"

# String#include? returns a boolean
s.include?("bar")   # => true
s.include?("hello") # => false

# String#[] with a string argument returns a string or nil
s["bar"]   # => "bar"
s["hello"] # => nil
```

Even better, all of these work with indexed assignment too, so you can replace specific parts of your string effortlessly!

```ruby
s = "foo bar baz"
s[/b../] = "hello"
s # => "foo hello baz"
```

# `String#tr`
The `tr` method on strings translates one set of characters into another.

When called on a string and given two arguments, it maps characters from the first argument found in the string to the corresponding characters in the second argument.

This method is best explained with an example. Suppose we want to replace all occurrences of `.` with `!`, and vice versa. We could use the following `tr` call.

```ruby
"Foo. Bar!".tr("!.", ".!") # => "Foo! Bar."
``` 

It can also be used to map ranges of characters onto one character, or onto another range. A caret (`^`) can be used as a "not" operator, too.

```ruby
"abcdef".tr("a-d", "x")   # => "xxxxef"
"abcdef".tr("a-d", "w-z") # => "wxyzef"
"abcdef".tr("^a-d", "x")  # => "abcdxx"
```

The `String#tr` method is a powerful method which can make fiddly string replacements much easier if used effectively! Like many string methods, it also comes with a mutating form, `String#tr!`.

# `String#squeeze`
The `squeeze` method removes adjacent duplicate characters in a string. By default, it will remove any characters, but it also takes an argument to select the range of characters to remove. This argument is in the same format is `#tr`'s arguments - `-` for ranges and `^` for inversion.

This is most useful as a flexible whitespace-removal tool, which is much easier to read than a regex.

```ruby
"aaabbaacccc".squeeze        # => "abac"
"aaabbaacccc".squeeze("a-b") # => "abacccc"

# One of the best uses...
"Look   at all  this   whitespace!".squeeze(" ") # => "Look at all this whitespace!"
```

----

I hope this has taught you something new! If you have any tips for dealing with Ruby strings effectively, leave them in a comment below.