1

I have a hash like this:

@password_constraints = {
  length: 'Must be at least 6 character long.',
  contain: [
      'one number',
      'one lowercase letter',
      'one uppercase letter.'
  ]
}

And I want to write a method that returns an human-readable string built from this hash like this one:

Must be at least 6 character long. Must contain: one number, one lowercase letter and an uppercase letter.

Currently, I have a very verbose method with an each that iterates over the @password_constraints[:contain] array and has several conditions to check if I have to put a ,, or an and or nothing.

But I want almost the same behavior as join(', ') but the last delimiter must be and.

I'm looking for some sort of solution like this one with the special join:

def password_constraints
  @password_constraints[:length] << ' Must contain.' << @password_constraints[:contain].join(', ') 
end

Is there a way to make this more compact or Ruby-like?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
soutoner
  • 571
  • 2
  • 14

3 Answers3

4

You can use the splat operator to split the array and just check the case if there's only one string:

def message(constraints)
  *others, second_last, last = constraints[:contain]
  second_last += " and #{last}" unless last.nil?
  "#{constraints[:length]} Must contain: #{(others << second_last).join(', ')}"
end

@password_constraints = {
  length: 'Must be at least 6 character long.',
  contain: [
      'one number',
      'one lowercase letter',
      'one uppercase letter.'
  ]
}

message(@password_constraints)
# => "Must be at least 6 character long. Must contain: one number, one lowercase letter and one uppercase letter." 

# if @password_constraints[:contain] = ['one lowercase letter', 'one uppercase letter.']
message(@password_constraints)
# => "Must be at least 6 character long. Must contain: one lowercase letter and one uppercase letter." 

# if @password_constraints[:contain] = ['one lowercase letter']
message(@password_constraints)
# => "Must be at least 6 character long. Must contain: one lowercase letter"
Doguita
  • 15,403
  • 3
  • 27
  • 36
  • This is definitely a more Ruby-like answer. Using the splat operator to pull the last element, and using string interpolation instead of concatenation is preferred for clarity. If you really wanted to get funky, you could change the second bit of interpolated code in the output string to be `"#{(others << second_last) * ', '}"`, but that's a lot harder to read. – Shotgun Ninja Sep 16 '15 at 18:57
  • Such an elegant solution and very compact. It's definitely what I was looking for. Thank you so much! – soutoner Sep 16 '15 at 19:07
  • I update the solution: the second line of the method should be removed and it should be at the end of the third one, in order to not modify the current hash. `*others, second_last, last = constraints[:contain]` `"#{constraints[:length]} Must contain: #{(others << second_last).join(', ')}" << " and #{last}" unless last.nil?` – soutoner Sep 19 '15 at 16:54
  • I don't know...You problably should create another question on SO with this new problem. But before that, if you are using Rails, you want to check [`Array#to_sentence`](http://api.rubyonrails.org/classes/Array.html#method-i-to_sentence) – Doguita Sep 19 '15 at 16:54
  • Thank you for the help Doguita, I've already found the problem.The problem was that I was actually modifying the hash every time I called the function hahah. Greetings! – soutoner Sep 19 '15 at 16:56
  • Hey @soutoner. Your solution had the same problem. I did an update and now it will work fine. – Doguita Sep 19 '15 at 17:17
  • It's strange because both of them works for me, it must be a strange "rails-thing" hahaha thank you for your super fast help! – soutoner Sep 19 '15 at 17:33
0

I would use a monkey patch here for succinctness (yeah my join_requirements method could probably look a little nicer).

class Array
  def join_requirements
    result = ""
    self.each do |requirement|
      if requirement == self.last
        result << "and " << requirement
      else
        result << requirement + ", "
      end
    end
    result
  end
end

Then in your code you just see this:

contain = ['one number','one lowercase letter','one uppercase letter.']
puts contain.join_requirements
# => one number, one lowercase letter, and one uppercase letter.
Mike S
  • 11,329
  • 6
  • 41
  • 76
  • Thank you so much for this solution, I've never heard before about monkey patches. It works perfectly. The problem is that in this project I only need this behavior for one method. But in other projects where the 'and problem' is more frequent, it will be the best solution! Thanks again. – soutoner Sep 16 '15 at 18:55
  • While this works, it isn't more compact, and is no more "Ruby like". – the Tin Man Nov 09 '15 at 17:06
  • @theTinMan that's fair, I did mention in the answer it wasn't the optimal way to write it. I just wanted to offer up monkey patching as a solution. – Mike S Nov 10 '15 at 06:48
-1

Here's a fun little shorthand trick in Ruby that you can do with arrays to cut out the need for a verbose join.

def password_constraints
  @password_constraints[:length] << ' Must contain.' << @password_constraints[:contain] * ", "
end

The Array class' * operator explicitly checks for a string second operand, then converts to calling join under the covers.

Community
  • 1
  • 1
Shotgun Ninja
  • 2,540
  • 3
  • 26
  • 32