lumos
lumos

Reputation: 223

Loop through multiple conditions until all of them are met

I'm using Ruby to get input from a user to provide new names for a list of files. I'm storing the names in an array, but before I store it, I have a series of conditions that I'm looping through to make sure that the user's input is valid. It essentially boils down to this (I've removed parts of the code that aren't relevant to the question):

puts "Rename file to:"
new_name = gets.chomp
new_name = check_input(new_name,@all_names)
@all_names << new_name   

def check_input(new_name,all_names)

    while new_name.match(/\s/)
        puts "Names must not contain spaces:"
        new_name = gets.chomp
    end

    while new_name.empty?
        puts "Please enter a name:"
        new_name = gets.chomp
    end

    while all_names.include? new_name
        puts "That name already exists. Please enter a different name:"
        new_name = gets.chomp
    end

    return new_name

end

Overall this works pretty well, but I want to make sure to loop through each "while" condition again and again, until all of the conditions are met. If, for instance, a name "abc" already exists, the user follows this order:

  1. enters "abc" => "That name already exists. Please enter a different name"
  2. enters "a b c" => "Names must not contain spaces"
  3. enters "abc" again =>

The last entry works successfully, but I don't want it to, since it's skipping over the condition that checks for duplicates. Is there a better way to loop through these conditions simultaneously, with each new entry?

Thank you for any help!

Upvotes: 1

Views: 1507

Answers (4)

Simple Lime
Simple Lime

Reputation: 11035

Right idea with the loop, just the wrong place for it. You need to check each gets from the user against all possible invalid cases. What you were doing was checking until a single invalid case was passed and then going on to a different one, which didn't check if the previous case(s) still passed:

# outputs an error message and returns nil if the input is not valid.
# Otherwise returns the input
def check_input(input, all_names)
  if input.match(/\s/)
    puts "Name must not contain spaces:"
  elsif input.empty?
    puts "Please enter a name:"
  elsif all_names.include?(input)
    puts "That name already exists. Please enter a different name:"
  else
    input
  end
end

@all_names = ['abc']
puts "Rename file to:"
# keep gets-ing input from the user until the input is valid
name = check_input(gets.chomp, @all_names) until name
@all_names << name
puts @all_names.inspect

Since puts returns nil, check_input will return nil if the input is not valid. Otherwise, in the final else, we'll return the valid input and assign it to the variable name and stop executing the until loop.

Example run:

Rename file to:
abc
That name already exists. Please enter a different name:
a b c
Name must not contain spaces:
abc
That name already exists. Please enter a different name:
abc23
["abc", "abc23"]

Upvotes: 3

Cary Swoveland
Cary Swoveland

Reputation: 110665

Code

def rename_files(fnames)
  fnames.each_with_object({}) do |fn,h|
    loop do
      puts "Rename file '#{fn}' to:"
      new_name = gets.chomp
      bad_name = bad_name?(new_name, h)
      if bad_name
        print bad_name
      else
        h.update(new_name=>fn)  
        break
      end
    end
  end.invert
end

def bad_name?(new_name, h)
  if new_name.include?(' ')
    "Names must not contain spaces. "
  elsif new_name.empty?
    "Names cannot be empty. "
  elsif h.key?(new_name)
    "That name already exists. Duplicates are not permitted. "
  else
    nil
  end
end

Example

rename_files(["cat", "dog", "pig"])
Rename file 'cat' to:
  # <enter "three blind mice">
Names must not contain spaces. Rename file 'cat' to:
  # <enter ENTER only> 
Names cannot be empty. Rename file 'cat' to:
  # <enter "three_blind_mice">
Rename file 'dog' to:
  # <enter "four_blind_mice">
Rename file 'pig' to:
  # <enter "three_blind_mice?>
That name already exists. Duplicates are not permitted. Rename file 'pig' to:
  # <enter "five_blind_mice"
  #=> {"cat"=>"three_blind_mice", "dog"=>"four_blind_mice", "pig"=>"five_blind_mice"}

Notes

  • bad_name? returns a (truthy) message string if the proposed file name is invalid for one of the three specified tests; else nil is returned.
  • if bad_name? returns a truthy value, it is printed using print, rather than puts, as it will be followed by puts "Rename file '#{fn}' to:" on the same line. The latter message is in part to remind the user which file is being renamed.
  • Hash.key? is used to determine if a proposed filename is a duplicate of one already entered, in part because hash key lookups are much faster than linear searches used to find array elements.
  • the new names may include the original names. Care therefore must be taken in renaming the files. (Consider, for example, "f1" changed to "f2" and "f2" changed to "f1".) If none of the original names are to be used as new filenames an additional test must be added to bad_name? (and fnames must be passed as a third argument to that method).
  • the hash being constructed is inverted (using Hash#invert) as a final step, so that the keys are the original filenames.

Upvotes: 1

Mike Fogg
Mike Fogg

Reputation: 589

Yep, recursion is the right way to do something like this I think. Just throw this in a test.rb file and run ruby test.rb:

@all_names = []

def check_name(name = nil)
    # Find out if it's invalid and why
    invalid_reason = if name.empty?
         "Please enter a name:"
    elsif name.match(/\s/)
        "Names must not contain spaces:"
    elsif @all_names.include?(name)
        "That name already exists. Please enter a different name:"
    end

    # Either return the name or ask for it again
    if invalid_reason
        puts invalid_reason
        name = check_name(gets.chomp)
    end

    # Once we have it return the name!
    name
end

puts "Rename file to:"
new_name = check_name(gets.chomp)
puts "Successfully storing name '#{new_name}'..."
@all_names << new_name

Let me know if that's doing what you were looking for!

Upvotes: 0

max pleaner
max pleaner

Reputation: 26758

This could be a good use for recursion (just showing one of the conditions here, the others are the same structure):

def check_input(new_name,all_names)

    # using if instead of while ... recursion provides the 'loop' here
    if new_name.match(/\s/)
        puts "Names must not contain spaces:"
        new_name = check_input(gets.chomp, all_names)
    end

    # etc, other conditionals

    new_name

end

Essentially, none of these new_name assignments will resolve until the input passes all the checks. The program moves deeper into the stack, but everything will resolve as soon as some input passes all the checks.

Upvotes: 0

Related Questions