My first (and worst) Rust program

I've been looking for an excuse to try out Rust for a while and decided to write a simple CLI to help split grocery costs between my partner and myself.

The Current Solution

We currently split grocery costs in 3 ways, she pays for her items, I pay for mine, and the food we share we split the cost fifty/fifty.

I receive an email receipt from our grocery store. I then enter the costs into Soulver to do some Maths and figure out how much we each owe.

The Problem

The current solution is slow and prone to human error (I get distracted). If I miss an item, add an item twice, or type in the wrong price, then one of us could end up owing too much or too little.

Although we share some food, one of us often eats more than the other of some products. In this situation the fifty fifty split is less fair.

Trying to split these items more further would make the calculation more tedious and error prone.

My Rust Solution

Since I receive an email with the receipt I can take the text and run it through a script quite easily.

The below Rust script, although terrible, seems to do the job just fine.

It takes a file and runs through each line. Splits the cost by looking for the £ sign on each line. Then asks who the item is for. Now with the added benefit of being able to do a 25/75 split for items that are used more by one person than the other. It also keeps a running total for each person which is a nice bonus.

Here's an example receipt. This is pasted directly from an email and very little is changed.

You ordered    1 X HECK 10 Meat-Free Magic 300g    £1.75
We sent    1 X HECK 10 Vegan Italia Chipolatas 300g    £1.75

Your order

Fridge    Quantity    Price
Curly Kale 150g    1   £0.50
Greek Style Yogurt 500g    1   £0.75
Soft Cheese 180g    1   £1.95
Fresh Whole Milk 1l    2   £2.80
Ready Rolled Filo Pastry 200g    1   £1.30
Extra Mature Cheddar Cheese 400g    1   £2.00
Fresh    Quantity    Price
Sweetclems 600g    1   £1.29
Freezer    Quantity    Price
Frozen Strawberries 350g    1   £1.75
Vegetarian 2 Cheese & Spring Onion Crispbakes 280g    1   £1.50
Straight Cut Chips 1.5kg    1   £2.00
Groceries, Health & Beauty and Household Items    Quantity    Price
Green Beans 240g    1   £0.90
Thick Bleach Citrus Burst 750ml    2   £0.78
Corn Flakes 450g    1   £1.89
Wholewheat Penne 500g    1   £0.53
Maple Syrup 250g    1   £4.50

And here's the Rust code

use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
use read_input::prelude::*;
use colored::*;
use currency::Currency;

fn main() {
    let mut one_pays = Currency::from_str("0.0").unwrap();
    let mut two_pays = Currency::from_str("0.0").unwrap();
    let file = std::env::args().nth(1).expect("no pattern given");
    if let Ok(lines) = read_lines(file) {
        // Consumes the iterator, returns an (Optional) String
        for line in lines {
            if let Ok(ip) = line {
                if ip.starts_with("You ordered") {
                    // do nothing
                } else if ip.starts_with("We sent") {
                    let item_text = ip.replace("We sent", "");
                    let split_row: Vec<&str> = item_text.trim().split("£").collect();
                    let length = split_row.len();
                    if length > 1 {
                        let name = &split_row[0].trim();
                        let cost = Currency::from_str(split_row[1].clone()).unwrap();
                        let (one_item_cost, two_item_cost) = ask(name, cost.clone());
                        one_pays = one_pays + one_item_cost;
                        two_pays = two_pays + two_item_cost;
                        running_total(&one_pays, &two_pays);
                    }
                } else if ip.ends_with("Price") {
                    // do nothing
                } else {
                    let split_row: Vec<&str> = ip.split("£").collect();
                    let length = split_row.len();
                    if length > 1 {
                        let name = &split_row[0].trim();
                        let cost = Currency::from_str(split_row[1].clone()).unwrap();
                        let one_item_cost, two_item_cost) = ask(name, cost);
                        one_pays = one_pays + one_item_cost;
                        two_pays = two_pays + two_item_cost;
                        running_total(&one_pays, &two_pays);
                    }
                }
            }
        }
    }
}
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

fn ask(item: &str, cost: Currency) -> (Currency, Currency) {
    let mut one_pays: Currency = Currency::from_str("0.0").unwrap();
    let mut two_pays: Currency = Currency::from_str("0.0").unwrap();
    println!("Who pays for {}? (£{})", item.bold(), cost);
    let answer: String = input().msg("m/k/b/m3/k3/ignore: ").get();
    println!("You said {}", answer.green());
    match answer.as_str() {
        "m" => {
            one_pays = one_pays + &cost;
            println!("one pays an additional £{}", &cost);
        },
        "k" => {
            two_pays = two_pays + &cost;
            println!("two pays an additional £{}", &cost);
        },
        "b" => {
            let split_cost = cost / 2;
            println!("Splitting cost: one pays £{}, two pays £{}", split_cost, split_cost);
            one_pays = one_pays + &split_cost;
            two_pays = two_pays + &split_cost;
        },
        "m3" => {
            let split_cost = cost / 4;
            println!("Splitting cost: one pays £{}, two pays £{}", &split_cost * 3, &split_cost);
            one_pays = one_pays + (&split_cost * 3);
            two_pays = two_pays + &split_cost;
        },
        "k3" => {
            let split_cost = cost / 4;
            println!("Splitting cost: one pays £{}, two pays £{}", &split_cost, &split_cost * 3);
            one_pays = one_pays + &split_cost;
            two_pays = two_pays + (&split_cost * 3);
        },
        _ => {
            println!("{}", "Invalid answer".red().bold());
            println!("{}", "Press 'i' to ignore".green());
            println!("");
            let (m, k) = ask(item, cost);
            one_pays = one_pays + m;
            two_pays = two_pays + k;
        }
    };
    (one_pays, two_pays)
}
fn running_total(one_pays: &Currency, two_pays: &Currency) -> () {
    println!("");
    println!("{}", "Running Total".bold());
    println!("-------------");
    println!("one: £{}", &one_pays);
    println!("two: £{}", &two_pays);
    println!("-------------");
    println!("");
}

Here's the Cargo.toml

[package]
name = "cost-splitter"
version = "0.1.0"
authors = ["Mitch"]
edition = "2018"

[dependencies]
read_input = "0.8"
colored = "2"
currency = "~0.4.0"

There you have it!

Alt Text

The Dependencies

I used three crates for this project.

Read Input

From what I can tell there's no easy way to read from stdin with Rust out of the box. This crate makes it really easy.

let answer: String = input().msg("m/k/b/m3/k3/ignore: ").get();

Colored

I added a bit of text formatting and colour with this crate.

println!("{} {} {}", "Text".green(), "Text 2".green().bold(), "Text 3".green());

Currency

I used this crate for handling the maths and formatting the output.

let cost = Currency::from_str(split_row[1].clone()).unwrap();

Overall experience with Rust so far

My experience developing this was pretty enjoyable. I was able to get a workable executable quite quickly with the help of the Rust compiler.

I did, however, struggle with a few items.

  • Using Color with the Currency crate. I gave up on this in the end.

  • Strings are quite confusing. Not so much ownership which is also a new concept to me, but the various types of strings and when to use them. I tried to create an array of ignored items/lines to output at the end of the execution but couldn't get it working. Hopefully, with experience I'll become more familiar with strings and how to handle this.

What next?

Aside from working on another Rust application I'm considering comparing this development experience with Go or Crystal. Go has piqued my interest—although I have no experience with it. I've used Crystal quite a lot and I suspect writing this application would be trivial in it.

Let me know if you'd be interested in hearing about me port the application to either of these two languages!