HiveBrain v1.2.0
Get Started
← Back to all entries
patternrustMinor

A Rusty implementation of CowSay

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
rustyimplementationcowsay

Problem

I'm making my first foray into Rust. I wanted to copy a fun and non utilitarian tool so I chose CowSay. It's not feature complete (CowSay has a lot of options) but it's a start.

My primary reasons for asking here are:

  • To get better at idiomatic Rust (this is the first thing I've written in the language)



  • Improve the implementation of the chunk_args and multi_line functions (either by improving them or making me aware that they falling prey to XY thinking)



(The code is available on github for your cloning pleasure)

main.rs

``
extern crate getopts;

use std::io::prelude::*;
use std::cmp;
use getopts::Options;

const PROGRAM_NAME: &'static str = "rsay";
const COW: &'static str = include_str!("cow.txt");
const DEFAULT_LINE_WIDTH: usize = 40;

fn print_usage (opts: Options) {
let brief = format!("Usage: {} [-OPTIONS] [ARG...]", PROGRAM_NAME);
print!("{}", opts.usage(&brief));
}

fn parse_numeric(value: String, default: usize) -> usize {
match value.parse::() {
Ok(n) => { n },
Err(_) => { default },
}
}

// Ensure that lines have a maximum length of max_size
// and that there is a one space buffer between args
// e.g. with a
max_width` of five, the phrase "prolong a hat bear"
// should be split like so:
// ["prolon", "ng a", "hat", "bear"]
fn chunk_args (args: Vec, max_size: usize) -> Vec {
let mut lines = Vec::with_capacity(args.len() * 2);
let remainder: String = args.iter()
.fold(String::new(), |mut acc, arg| {
if !acc.is_empty() {
if (arg.chars().count() + 1) + acc.chars().count() , width: usize) -> String {
let total_length = lines.len() - 1;

let formatted_lines = lines
.iter()
.enumerate()
.map(|(idx, line)| {
let current_length = line.clone().chars().count();
let padding: String = (0..width - current_length).map(|_| ' ').collect();
let (start, end) = match idx {
0 => ('/', '\\'),

Solution

-
The tests fail. That's not a good thing:

---- test_say_multi_line stdout ----
thread 'test_say_multi_line' panicked at 'assertion failed:
(left == right)
(left:
" _______\n/ broke \\\n| n big |\n\\ bar /\n-------",
right:
" _______\n/ broke \\\n| n big |\n\\ bar /\n -------")', src/main.rs:153

---- test_say_single_line stdout ----
thread 'test_say_single_line' panicked at 'assertion failed:
(left == right)
(left:
" _____________\n\n-------------",
right:
" _____________\n\n -------------")', src/main.rs:171


-
The output is not as described or expected. I don't even see the code that would move the cow over...

$ rsay moo a lot more than that --width=10

___________
/ moo a lot \
| more than |
\ that /
-----------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||


-
There's a warning. Don't ignore warnings; fix them. One of the reasons to have a compiled language is to have the feedback from the compiler.

warning: unused import, #[warn(unused_imports)] on by default
use std::io::prelude::*;
^~~~~~~~~~~~~~~~~~~~


-
The code makes the assumption that a single char takes a width of 1. Zero-width spaces would disagree, and there are probably code points wider than one.

-
Splitting strings on Unicode code points may not make any sense. For example, if the input contains a letter with a combining diacritic, it doesn't make sense to split those on separate lines. Perhaps a combination of Unicode normalization and graphemes would help.

-
Accept a reference to a non-Copy type unless the code directly benefits from transferring ownership. For example, accept &str instead of String and &[T] instead of a Vec. These are more flexible.

-
Combining the above suggestion for Vec is a bit more complicated; instead, the code can accept anything that can look like a &str using AsRef. Note how doing this simplifies tests and avoids extra allocation.

-
There's no need to specify a type on parse in parse_numeric; it's inferred from the function return type.

-
Become familiar with the methods on Option and Result.

  • Providing a default is unwrap_or.



  • Converting a value when it is present is map.



  • Combining these is map_or.



  • Chaining multiple Options or Results is and_then.



  • Panicking when the value is missing is unwrap or even better expect.



-
Once these methods are used, parse_numeric has questionable benefit. I'd inline it, which also avoids duplicating the default value.

-
If a function is documented for the consumer, use the Rustdoc comment (///). Documentation is Markdown, so use things like foo to highlight code and arguments consistently.

-
The documentation for chunk_args demonstrates that a "max width of 5" produces a 6-character string. And the "ng" of prolong is repeated.

-
The documentation demonstrates chunk_args is provided with a string, but it appears to be a Vec?

-
The tests should show the case of a single string with multiple words; it doesn't appear that rsay "moo alot more than that" --width=10 is handled well.

-
Avoid duplicating function argument and return types and tests / examples in the function documentation when possible; code can't lie as easily as docs.

-
Remember that Rustdoc can contain code that is executed at compile time. This is often used for inline tests to ensure the examples don't rot.

-
There are no spaces between the function name and the parenthesis. Check out rustfmt.

-
total_length seems misnamed; should be something like last_index.

-
There's an unneeded clone when counting chars.

-
Extract a repeat function. I like this because it avoids the need to specify collecting to a String at all the call sites.

-
I'd normally use repeat instead of mapping a range, but I'm not sure that matters.

-
There's no need to specify the type of the Vec children when collecting the arguments; it can be inferred.

-
Simplify the logic around checking is_empty; it can just be an early exit.

-
The #[cfg(test)] isn't really doing anything; it's only applied to the next item. The next item happens to be a #[test] function anyway, which will automatically be removed when not compiling in test mode. Normally, the cfg attribute is applied to a test module.

-
String and &str can be directly compared; there's no need to allocate.

``
extern crate getopts;

use std::cmp;
use getopts::Options;

const PROGRAM_NAME: &'static str = "rsay";
const COW: &'static str = include_str!("cow.txt");
const DEFAULT_LINE_WIDTH: usize = 40;

fn print_usage(opts: Options) {
let brief = format!("Usage: {} [-OPTIONS] [ARG...]", PROGRAM_NAME);
print!("{}", opts.usage(&brief));
}

/// Ensure that lines have a maximum length of
max_size` and that
/// there is a one space buffer between args.
fn chunk_args(args: &[S], max_size: usize) -> Vec
where S: AsRef,
{
let mut li

Code Snippets

extern crate getopts;

use std::cmp;
use getopts::Options;

const PROGRAM_NAME: &'static str = "rsay";
const COW: &'static str = include_str!("cow.txt");
const DEFAULT_LINE_WIDTH: usize = 40;

fn print_usage(opts: Options) {
    let brief = format!("Usage: {} [-OPTIONS] [ARG...]", PROGRAM_NAME);
    print!("{}", opts.usage(&brief));
}

/// Ensure that lines have a maximum length of `max_size` and that
/// there is a one space buffer between args.
fn chunk_args<S>(args: &[S], max_size: usize) -> Vec<String>
    where S: AsRef<str>,
{
    let mut lines = Vec::with_capacity(args.len() * 2);
    let remainder: String = args.iter()
        .fold(String::new(), |mut acc, arg| {
            let arg = arg.as_ref();

            if !acc.is_empty() {
                if (arg.chars().count() + 1) + acc.chars().count() <= max_size {
                    return acc + " " + arg;
                } else {
                    lines.push(acc.clone());
                    acc.clear();
                }
            }

            for c in arg.chars() {
                acc.push(c);
                if acc.chars().count() == max_size {
                    lines.push(acc.clone());
                    acc.clear();
                }
            }

            acc
        });

    if !remainder.is_empty() {
        lines.push(remainder);
    }

    lines
}

fn repeat(s: &str, len: usize) -> String {
    ::std::iter::repeat(s).take(len).collect()
}

/// Add the proper border to each line.
///
/// ["first", "mid", "last"] would become
///
/// / first \
/// | mid   |
/// \ last  /
fn multi_line<S>(lines: &[S], width: usize) -> String
    where S: AsRef<str>,
{
    let last_index = lines.len() - 1;

    let formatted_lines = lines.iter()
        .enumerate()
        .map(|(idx, line)| {
            let line = line.as_ref();
            let current_length = line.chars().count();
            let padding = repeat(" ", width - current_length);
            let (start, end) = match idx {
                0 => ('/', '\\'),
                _ if idx == last_index => ('\\', '/'),
                _ => ('|', '|'),
            };

            format!("{} {}{} {}\n", start, line, padding, end)
        });

    formatted_lines.collect()
}

fn say<S>(args: &[S], desired_width: usize) -> String
    where S: AsRef<str>,
{
    let chunks = chunk_args(args, desired_width);
    let largest_str = chunks.iter().map(|x| x.chars().count()).max();
    let width = largest_str.map_or(desired_width, |x| cmp::min(desired_width, x));

    let formatted = match chunks.len() {
        1 => format!("< {} >\n", chunks.join(" ")),
        _ => multi_line(&chunks, width),
    };
    let top_border = repeat("_", width + 2);
    let bottom_border = repeat("-", width + 2);

    format!(" {}\n{} {}", top_border, formatted, bottom_border)
}

fn main() {
    let args: Vec<_> = std::env::args()
        .skip(1)
        .collect();
    let mut opts = Options::new();

    opts.optflag("h", "help", "Print this help menu");
  

Context

StackExchange Code Review Q#135796, answer score: 9

Revisions (0)

No revisions yet.