Error Handling in Rust

There are a lot of things to love about Rust, not least of which are its excellent documentation and package handler (Cargo). But I also really like the error handling.

In Rust, you can choose to handle the error via having the program panic (which will abort the entire program), but this is generally considered something best avoided – especially if you are writing a program that other people will be using.

if n < 1 || n > 10 {
        panic!("Invalid number: {}", n);
    }

For example, if you had this in your code, and you passed in a number lower than n or greater than 10, you would not get a very good error message and it would be annoying.

An exception would be if the error is raised by something that should not happen and therefore indicating an actual bug in the program (and not simply the user typing the wrong thing, etc). In this case, you would want your program to panic and stop everything – because there is something way wrong that you didn’t foresee (and so probably could not have written a specific case for it).

You can unwrap something, which will either return the result of a computation or panic. But this, of course, runs into a lot of the same annoying things as panic.

You can also specify that, if you run into an error, for the program to simply continue. For example:

fn main() {
    println!("Guess the number!");
    println!("Enter your guess:");

    let secret_number = rand::thread_rng().gen_range(1, 101);
    
    loop {
        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
        .expect("Failed to read input");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Higher!"),
            Ordering::Greater => println!("Lower!"),
            Ordering::Equal => {
                println!("You Win!");
                break;    
            }
        }
    }
}

If the person types in something that is a number, then it will be compared to the randomly generated number and told whether they have won or not. But if the person types in something that is not a number, then the the program will continue and the rest of the loop will simply skip and then ask the user to enter another number.

And there are many more ways to handle errors in Rust. You can write error messages for what happens when no arguments were passed when they were expected, or to display a different error if the argument fails to parse (and therefore is not the appropriate type), or to simply proceed as normal if everything checks out:

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
        .map(|n| 2 * n)
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

And there are many more ways to write helpful error messages and to handle the errors in such a way that your program behaves the way it should (which includes panic-ing when needed). This is just another part of what makes Rust so powerful and such a pleasure to work with.

Continuing on Memory Lane with Dungeon Keeper 2

I’ve been continuing on down memory lane with Dungeon Keeper II, and have almost finished with the campaign. I just finished storming the Faerie Fortress.

imps-digging
Imps breaking down the back wall of the Faerie Fortress

It has been a lot of fun reliving the campaign. Soon now, I will get to the Dark Angel part in the campaign!

Google Location History Data Part IV

I’ve returned to my Google Location History Data (the previous installment of which is here), to implement something I had been thinking about for a while: setting a maximum time threshold for how far apart (temporally) two coordinate pairs (of the same activity type) can be and still be considered part of the “same trip.” Figuring out this threshold isn’t as obvious as I was originally thinking, but I still think I got something meaningful out of this exercise. I think a spread of 10 minutes obviously encompasses the same trip, but what about an hour? In many cases, likely not.

Recall that of ~1,000,000 data points (over 3 years), only 288,922 had any activities associated with them. This means, for the 1,000,000 data points, I have (on average) one point for every 31 seconds. For the 288,922 activity-associated data points, I have (on average) one point for every 109 seconds. So, the threshold will have to at least be 109 seconds, but most likely higher.

So I futzed around with the threshold a lot to see how that changed the results. I think 15-20 minutes is a pretty good threshold without allowing a ton of noise in, but the average miles/day for all the activities still seems pretty low with that threshold. The true number might lie somewhere closer between 20m and 1h. Or maybe I just don’t go as far as I imagine I do!

I wrote the threshold decider stuff in Ruby, because I had initially written the “sort-by-activities” script in Ruby (this was before I knew I was going to use Python for Basemap etc). And I had already written the Haversine script in Ruby, and I wanted to reuse that. Oh, well. Maybe one day I will normalize everything to be in Python, for consistency’s sake.

#Uses the Haversine formula to calculate the distance between two lat, long coordinate pairs
def haversine(old_lats_longs, new_lats_longs)
  lat1 = old_lats_longs[0]
  lon1 = old_lats_longs[1]
  lat2 = new_lats_longs[0]
  lon2 = new_lats_longs[1]

  r = 6371000
  phi1 = (lat1*Math::PI)/180
  phi2 = (lat2*Math::PI)/180

  deltaPhi = ((lat2-lat1)*Math::PI)/180

  deltaLambda = ((lon2-lon1)*Math::PI)/180

  a = Math.sin(deltaPhi/2) * Math.sin(deltaPhi/2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda/2) * Math.sin(deltaLambda/2)
  c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))

  distance = r * c

  return distance
end

#Decides whether to count a dated coordinate as part of the same trip or not, based on the time threshold
def threshold_decider(dated_coords) #dated_coords is an array of coordinate pairs with their time, format: [time,[lat,long]]
  threshold = 1000 #Set a maximum threshold (in seconds) for a coordinate to be counted in the same trip
  distance = 0
  total_time = 0
  time_period = dated_coords.first[0]-dated_coords.last[0]

  previous_time = dated_coords.first[0]
  previous_lats_longs = dated_coords.first[1]

  dated_coords.each do |dated_coord|
    (distance+=haversine(previous_lats_longs, dated_coord[1])) && (total_time+=(previous_time-dated_coord[0])) if previous_time-dated_coord[0] <= threshold
    previous_time = dated_coord[0]
    previous_lats_longs = dated_coord[1]
  end
end

This is the meat of the threshold decider. Pretty simple. Almost exactly identical to the old Haversine distance calculator I wrote to get me aggregate distances, except the distances only get calculated/added if they are within the temporal threshold.

This is pretty much the context I have it in right now, with the file-reader and human-friendly-displayer (et al) to quickly display some of the stats that I am interested in seeing:

require 'time'

#Uses the Haversine formula to calculate the distance between two lat, long coordinate pairs
def haversine(old_lats_longs, new_lats_longs)
  lat1 = old_lats_longs[0]
  lon1 = old_lats_longs[1]
  lat2 = new_lats_longs[0]
  lon2 = new_lats_longs[1]

  r = 6371000
  phi1 = (lat1*Math::PI)/180
  phi2 = (lat2*Math::PI)/180

  deltaPhi = ((lat2-lat1)*Math::PI)/180

  deltaLambda = ((lon2-lon1)*Math::PI)/180

  a = Math.sin(deltaPhi/2) * Math.sin(deltaPhi/2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda/2) * Math.sin(deltaLambda/2)
  c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))

  distance = r * c

  return distance
end

#Decides whether to count a dated coordinate as part of the same trip or not, based on the time threshold
def threshold_decider(dated_coords) #dated_coords is an array of coordinate pairs with their time, format: [time,[lat,long]]
  threshold = 1000 #Set a maximum threshold (in seconds) for a coordinate to be counted in the same trip
  distance = 0
  total_time = 0
  time_period = dated_coords.first[0]-dated_coords.last[0]

  previous_time = dated_coords.first[0]
  previous_lats_longs = dated_coords.first[1]

  dated_coords.each do |dated_coord|
    (distance+=haversine(previous_lats_longs, dated_coord[1])) && (total_time+=(previous_time-dated_coord[0])) if previous_time-dated_coord[0] <= threshold
    previous_time = dated_coord[0]
    previous_lats_longs = dated_coord[1]
  end

  display(time_period, total_time, distance)
end

#Reads the file of sorted Google Location History data
def reader()
  dated_coords = [] #An array of coordinate pairs with their timestamp, format: [timestamp,[lat,long]]

  File.open("inVehicle.txt", 'r') do |file|
    file.each_line do |line|
      columns = line.split("\t")
      dated_coords << [Time.parse(columns[0]),[columns[1].to_f,columns[2].to_f]]
    end
  end

  threshold_decider(dated_coords)
end

def sec_to_year(seconds)
  seconds/31536000
end

def sec_to_hour(seconds)
  seconds/3600
end

def sec_to_day(seconds)
  seconds/86400
end

def m_to_km(meters)
  meters/1000
end

def m_to_mi(meters)
  meters/1609
end

#Displays all the information in a way that humans like to read
def display(time_period, total_time, distance)
  puts "The time period was #{sec_to_year(time_period).round(2)} years!"
  puts "The total distance gone over that full time period was #{m_to_km(distance).round} kilometers, or #{m_to_mi(distance).round} miles!"
  puts "You spent #{sec_to_hour(total_time).round} hours doing it!"
  puts "That is an average of #{(m_to_mi(distance)/sec_to_day(time_period)).round(2)} miles per day!"
  puts "You've spent #{((total_time/time_period)*100).round(2)}% of your time doing this activity!"
  puts "You've averaged #{(m_to_mi(distance)/sec_to_hour(total_time)).round}mph!"
end

reader()

So! Let us see some of my results I got with the threshold set to 1,000 seconds.

Walking:

The time period was 2.8 years!

The total distance gone over that full time period was 5543 kilometers, or 3445 miles!

You spent 1864 hours doing it!

That is an average of 3.37 miles per day!

You’ve spent 7.6% of your time doing this activity!

You’ve averaged 2mph!

Bicycling:

The time period was 2.79 years!

The total distance gone over that full time period was 3047 kilometers, or 1894 miles!

You spent 297 hours doing it!

That is an average of 1.86 miles per day!

You’ve spent 1.22% of your time doing this activity!

You’ve averaged 6mph!

In a Vehicle:

The time period was 2.8 years!

The total distance gone over that full time period was 31394 kilometers, or 19511 miles!

You spent 1299 hours doing it!

That is an average of 19.12 miles per day!

You’ve spent 5.3% of your time doing this activity!

You’ve averaged 15mph!

All sounds pretty reasonable to me! Except maybe the speeds for all three seem pretty low. Likely because I am already including lots of time periods of me not moving. But the distances/day seem like roughly what I would expect to see. Anyhow, not much I can do with this now except futz with the threshold and see what seems most reasonable.