zone-detect 1.1.0

Rust version of the ZoneDetect C library.
Documentation
use rand::{Rng, SeedableRng, rngs::StdRng};
use std::{
    env,
    path::{Path, PathBuf},
    process::Command,
};
use zone_detect::{Database, Location, ZoneMatch, ZoneMatchKind};

fn random_location(rng: &mut StdRng) -> Location {
    Location {
        latitude: rng.random_range(-90.0..90.0),
        longitude: rng.random_range(-180.0..180.0),
    }
}

fn get_demo_path() -> PathBuf {
    let path_var = env::var("ZONEDETECT_DEMO").expect("the ZONEDETECT_DEMO env var must be set to the path of the ZoneDetect demo");
    let path = Path::new(&path_var);
    assert!(path.exists(), "ZoneDetect demo path is invalid");
    path.into()
}

fn lookup_result_to_string(result: ZoneMatchKind) -> &'static str {
    match result {
        ZoneMatchKind::InZone => "In zone",
        ZoneMatchKind::InExcludedZone => "In excluded zone",
        ZoneMatchKind::OnBorderVertex => "Target point is border vertex",
        ZoneMatchKind::OnBorderSegment => "Target point is on border",
    }
}

fn lookup(db: &Database, location: Location) -> (String, Vec<ZoneMatch>) {
    let mut output = String::new();
    let result = db.lookup(location);
    for result in &result.matches {
        output += &format!("{}:\n", lookup_result_to_string(result.kind));
        output += &format!("  meta: {}\n", result.zone.meta_id);
        output += &format!("  polygon: {}\n", result.zone.polygon_id);
        output += &format!(
            "  TimezoneIdPrefix: {}\n",
            result.zone.fields.get("TimezoneIdPrefix").unwrap()
        );
        output += &format!(
            "  TimezoneId: {}\n",
            result.zone.fields.get("TimezoneId").unwrap()
        );
        output += &format!(
            "  CountryAlpha2: {}\n",
            result.zone.fields.get("CountryAlpha2").unwrap()
        );
        output += &format!(
            "  CountryName: {}\n",
            result.zone.fields.get("CountryName").unwrap()
        );
    }
    output += &format!("Safezone: {:.6}\n", result.safezone);
    output += &format!(
        "The simple string is [{}]\n",
        db.simple_lookup(location).unwrap()
    );
    (output, result.matches)
}

/// This test compares the output against that of the demo program in
/// ZoneDetect by feeding both many pairs of random coordinates.
#[test]
#[ignore]
fn test_compare() {
    let path = get_demo_path();
    let db_path = "data/timezone21.bin";
    let db = Database::open(db_path).unwrap();

    let mut rng = StdRng::from_seed([0; 32]);

    let num_iterations = 10000;
    for i in 0..num_iterations {
        let loc = random_location(&mut rng);
        let output = Command::new(&path)
            .arg(db_path)
            .arg(loc.latitude.to_string())
            .arg(loc.longitude.to_string())
            .output()
            .unwrap();
        assert!(output.stderr.is_empty());
        let expected = std::str::from_utf8(&output.stdout).unwrap();
        let (actual, results) = lookup(&db, loc);
        if actual != expected {
            println!("expected: {expected}");
            println!("actual: {actual}");
            dbg!(&results);
        }
        assert_eq!(
            actual, expected,
            "failed on i={}, lat={}, lon={}, results={:?}",
            i, loc.latitude, loc.longitude, results,
        );
    }
}