rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.04k stars 12.69k forks source link

Add input casting function to standard library #117852

Open bazylhorsey opened 11 months ago

bazylhorsey commented 11 months ago

Willing to submit the PR myself with guidance. Here's my idea:

use std::io::{self, Write};
use std::str::FromStr;
use std::fmt::{self, Debug, Display, Formatter};
use std::error::Error;

/// Custom error type for input function.
#[derive(Debug)]
pub enum InputError {
    IoError(io::Error),
    ParseError(String),
}

impl Display for InputError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            InputError::IoError(e) => write!(f, "IO error: {}", e),
            InputError::ParseError(e) => write!(f, "Parse error: {}", e),
        }
    }
}

impl Error for InputError {}

impl From<io::Error> for InputError {
    fn from(e: io::Error) -> Self {
        InputError::IoError(e)
    }
}

/// A function similar to Python's `input`, allowing for a prompt and reading a line from stdin.
/// 
/// # Arguments
///
/// * `prompt` - An optional prompt to display before reading input.
/// * `flush` - Whether to flush stdout after displaying the prompt.
/// 
/// # Errors
///
/// This function will return an error if there's an issue with reading from stdin
/// or parsing the input into the desired type.
pub fn input<T: FromStr>(prompt: Option<&str>, flush: bool) -> Result<T, InputError>
where
    T::Err: Display {
    if let Some(p) = prompt {
        print!("{}", p);
        if flush {
            io::stdout().flush()?;
        }
    }

    let mut input = String::new();
    match io::stdin().read_line(&mut input) {
        Ok(0) => Err(InputError::IoError(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF reached"))),
        Ok(_) => input.trim().parse::<T>().map_err(|e| InputError::ParseError(e.to_string())),
        Err(e) => Err(e.into()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn get_int() {
        let i: i32 = input(None, false).unwrap();
        println!("{}", i);
        let j: i32 = input(None, true).unwrap();
        println!("{}", j);

    }

    #[test]
    fn get_int_2() {
        let i: i32 = input(Some("Input: "), false).unwrap();
        print!("Output: {}", i);
    }

    #[test]
    fn hacker_rank() {
        let s: String = input(None, true).unwrap();
        println!("hello, {}", s);
    }

}
bazylhorsey commented 11 months ago

To add topics for conversation here are some reasons for this feature:

  1. Introduction to language - This is an exceedingly common use-case for testing dynamic binary programs that execute off main. Using the test macros requires all function to be independent of main which all requires time learning. In languages like Python, input is one of the first ways shown to test programs with dynamic data, and easily castable. I'd argue this is an important tools for learning, POC, tests, and in some cases production with external processes (point 3).
  2. Low overhead - Uses all stable and core features. I'm not sure what module this would live in source, but this implementation could probably be even smaller with a smart rustecean's help.
  3. Built to work with the shell - Programmers should be able to pipe in other data without worrying about the cast themselves in the program or having to use a full-scale CLI library like clap.
  4. Wide-spreading use - Works to the full extent of primitives that implement FromStr trait for maximum flexibility.
  5. Small scope - this is a small functional change that adds ergonomics, but has low code-interaction and maintenance; mostly relying on system-level io.
ShE3py commented 11 months ago

Hi,

I think InputError should be generic;

#[derive(Debug)]
pub enum InputError<T: FromStr> {
    Io(io::Error),
    ParseError(<T as FromStr>::Err),
}

And is there an use case for not wanting to flush a non-empty prompt?


io::stdout().write_all(p.as_bytes())?; // `print!` may `panic!`

Alternative names may be ask or prompt (imo input is too ambiguous/vague).

bazylhorsey commented 11 months ago
use std::io::{self, Write};
use std::str::FromStr;
use std::fmt::{self, Debug, Display, Formatter};
use std::error::Error;

/// Custom error type for prompt function.
#[derive(Debug)]
pub enum PromptError<T: FromStr> 
where
    T::Err: Debug + Display,
{
    Io(io::Error),
    ParseError(<T as FromStr>::Err),
}

impl<T: FromStr> Display for PromptError<T>
where
    T::Err: Debug + Display,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            PromptError::Io(e) => write!(f, "IO error: {}", e),
            PromptError::ParseError(e) => write!(f, "Parse error: {}", e),
        }
    }
}

impl<T: FromStr + Debug> Error for PromptError<T> 
where
    T::Err: Debug + Display,
{
}

impl<T: FromStr> From<io::Error> for PromptError<T>
where
    T::Err: Debug + Display,
{
    fn from(e: io::Error) -> Self {
        PromptError::Io(e)
    }
}

/// A function to prompt for prompt and read a line from stdin, parsing it into a specified type.
///
/// # Arguments
///
/// * `prompt` - A prompt to display before reading prompt.
///
/// # Errors
///
/// This function will return an error if there's an issue with reading from stdin
/// or parsing the prompt into the desired type.
pub fn prompt<T: FromStr>(prompt: &str) -> Result<T, PromptError<T>>
where
    T::Err: Debug + Display,
{
    io::stdout().write_all(prompt.as_bytes())?; // `print!` may `panic!`
    io::stdout().flush()?; // Always flush after the prompt

    let mut prompt = String::new();
    match io::stdin().read_line(&mut prompt) {
        Ok(0) => Err(PromptError::Io(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF reached"))),
        Ok(_) => prompt.trim().parse::<T>().map_err(PromptError::ParseError),
        Err(e) => Err(e.into()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn get_int() {
        let i: i32 = prompt("").unwrap();
        println!("{}", i);
        let j: f32 = prompt("").unwrap();
        println!("{}", j);

    }

    #[test]
    fn get_int_2() {
        let i: i32 = prompt("Input: ").unwrap();
        print!("Output: {}", i);
    }

    #[test]
    fn hacker_rank() {
        let s: String = prompt("").unwrap();
        println!("hello, {}", s);
    }

}

Thanks so much @ShE3py!!! Does this look better? I'd like to know what you think about Debug + Display implementations on these, does this follow Rust source culture? Could you see this moving into the standard library, if so which library should it live in? And is there documentation on revising std with new functions?