Validate User Input against predefined "list"

I am converting a Python script to Rust. This script takes a user input and eventually update the rc.conf file (NetBSD). There are two types of inputs (for my app), service and flags. For example a valid service input would be something like, dbus=YES. I know I can "split" on the equal sign to grab the service value (YES). There are only a few valid service values, ["YES", "TRUE", "ON", "1", "NO", "FALSE", "OFF", "0"]. In Python I would simply do something like:

if value in valid_service_list: do something

How would I do that with Rust?

Okay... I found this, but I have no idea how or why it works:

fn main() {
    let service_values = ["YES","NO","TRUE","FALSE","ON","OFF","0","1"] ;
    println!("Find value in service_values: {:?}", service_values.iter().find(|&&x| x == "YES"));
}

With your code you are checking whether "YES" is contained in service_values, and I am not sure it is what you expect.

Anyway, if you use cargo clippy you will receive a hint to use .any in order to check if your value is inside service_value. Something like the following:

fn is_valid_value(v: &str) -> bool {
    const SERVICE_VALUES: [&str; 8] = ["YES", "NO", "TRUE", "FALSE", "ON", "OFF", "0", "1"];
    SERVICE_VALUES.iter().any(|x| *x == v)
}

As you can see, in this case const can be used because all the possible values are known a priori. Here a small sample playground to show you the behaviour.

2 Likes

WOW! Thank you very much. I don't use foul language; however, Rust seems to be able to bring me to the edge. This makes a lot of sense. I really appreciate it.

1 Like

For those looking for a more scalable solution (i.e., avoiding a linear search), you can use a Set using perfect hash functions, thanks to the ::phf crate:

#![feature(proc_macro_hygiene)] // currently requires nightly, should be fixed soon

fn is_valid_command (s: &'_ str) -> bool
{
    static VALID_COMMANDS: ::phf::Set<&'static str> = ::phf::phf_set! {
        "YES", "NO", "TRUE", "FALSE", "ON", "OFF", "0", "1",
    };

    VALID_COMMANDS.contains(s)
}
6 Likes

Thank you very much. This is way over my head, but I have already tucked it away in my notes for future use.

Another solution for all the noobs (like me) that might search this forum for a similar question. I am open to critics if this isn't really the Rusty way of doing things.

fn main() {
    let service_input = String::from("YES") ;
    println!("{:?}",is_valid_service(service_input)) ;
    println!("{:?}",is_valid_service(String::from("FALSE"))) ;
    println!("{:?}",is_valid_service(String::from("0"))) ;
    println!("{:?}",is_valid_service(String::from("enable"))) ;
    println!("{:?}",is_valid_service(String::from("yes").to_uppercase())) ;

}

fn is_valid_service(value: String) -> bool {
    match value.as_ref() {
        "YES" => true,
        "NO"  => true,
        "TRUE"  => true,
        "FALSE" =>true,
        "ON" => true,
        "OFF"  => true,
        "0" => true,
        "1"  => true,
        _ => false,
    }

}

I think this is a pretty Rust-y way of doing things :slight_smile: The main improvements I think you could make are:

  • If you only require read-only access to a string in a function, it's more flexible to take &str instead of String. This also has the nice side effect of allowing you to pass in string literals, as all string literals are also of type &str.
  • You can combine patterns in a match using |.
fn main() {
    // You can pass in a reference to a String...
    let service_input = String::from("YES");
    println!("{:?}", is_valid_service(&service_input));
    
    // Or a string literal...
    println!("{:?}", is_valid_service("FALSE"));
    println!("{:?}", is_valid_service("0"));
    println!("{:?}", is_valid_service("enable"));

    // You can call `to_uppercase` on a string literal too - it returns
    // a String, though, so don't forget the & to reference it!
    println!("{:?}", is_valid_service(&"yes".to_uppercase()));
}

fn is_valid_service(value: &str) -> bool {
    match value {
        "YES" | "NO" | "TRUE" | "FALSE" | "ON" | "OFF" | "0" | "1" => true,
        _ => false,
    }
}
1 Like

Just wanted to thank you all for your help. I wrote a little blog on what I learned. I hope it is accurate.

Ronverbs - Search For Value In An Array

Ok, time to explain this sentence:

Linear search

You start your post with:

More precisely,

  1. we define a sequence of valid commands (you mention a list, but since mutation of the list does not seem to be required, we can use Python's immutable sequence, i.e., a tuple):

    valid_services_sequence = (
         "YES", "NO", "TRUE", "FALSE", "ON", "OFF", "0", "1",
    )
    
  2. and we can then test the validity of a command with

    command in valid_services_sequence
    
    • Python's in operator is sugar for a call to the __contains__ method:

      valid_services_sequence.__contains__(command)
      
    • in the case of a sequence like list or tuple, this in turn becomes:

      any(command == valid_command for valid_command in valid_services_sequence)
      
    • and the equivalent Rust code is:

      /// `static` is more performant than `let`
      static valid_services_sequence: &[&'static str] = &[
          "YES", "NO", "TRUE", "FALSE", "ON", "OFF", "0", "1",
      ];
      

      followed by

      • either:

        valid_services_sequence.contains(&command);
        
      • or, equivalently:

        valid_services_sequence.iter().any(|&valid_command| command == valid_command);
        
      • which unrolls to:

        match &command {
            | "YES" | "NO" | "TRUE" | "FALSE" | "ON" | "OFF" | "0" | "1" => true,
            | _ => false,
        }
        

This is called a linear search: the (average) cost of the search evolves linearly with the amount of valid commands. In other words, if we double the amount of valid commands, it takes (on average) twice as long to know whether a given command is valid or not.

Now, given your example, with a sample size of only 8 valid commands, this is completely fine and actually the fastest way.

Using a Set

However, for people having a need similar to yours, but where their amout of "valid answers" is gigantic,
there is a then faster solution: using a Set. This is a collection where its elements are laid out in memory so that this very test (.contains(&element)) is much faster:

  • when implemented as an Ordered tree, its (axiomatic) cost is logarithmic w.r.t. the amount of valid commands: to double the processing time of a thousand elements the set would need to contain a million elements (vs two thousands in the linear case);

    • this is almost equivalent to having a sorted sequence to begin with, and using a binary search to test the presence of an element;
  • when implemented as a HashSet, the cost does not depend on the number of elements, except when hash collisions are involved.

    • In Python to create a (hash) set all you need to do is use braces instead of brackets or parenthesis:
      valid_services_set = {
          "YES", "NO", "TRUE", "FALSE", "ON", "OFF", "0", "1",
      }
      

To fix this issue with collisions, that prevents the cost of a contains(&element) test from truly ignoring the number of elements in the set, there exist perfect hash sets (and maps) based on perfect hash functions. These are only usable when the elements to be put in the set are known beforehand. Which is the case here, hence my suggesting:

6 Likes

Just because it shows off one of the more interesting implications of the way match pattern syntax works, here's another example that's very nice to use when there are a small number of known-in-advance options.


let user_input = ....;

let valid = if let "YES" | "NO" | "ON" | "OFF" = user_input { 
   true 
} else { 
   false 
};

tweak as needed for a fn return, eg:

fn valid (user_input: &str) -> bool {
  if let "YES" | "NO" | "ON" | "OFF" = user_input { 
    return true;
  }; 

  // maybe other ways to be valid..

  false 
};
3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.