diff --git a/rust/src/interface.rs b/rust/src/interface.rs index 9508cd4a..1343fe5b 100644 --- a/rust/src/interface.rs +++ b/rust/src/interface.rs @@ -102,7 +102,7 @@ pub fn is_full(code: &str) -> bool { pub fn point_to_integers(pt: Point) -> (i64, i64) { let (lng, lat) = pt.x_y(); - let mut lat_val = (lat * LAT_INTEGER_MULTIPLIER as f64).round() as i64; + let mut lat_val = (lat * LAT_INTEGER_MULTIPLIER as f64).floor() as i64; lat_val += LATITUDE_MAX as i64 * LAT_INTEGER_MULTIPLIER; if lat_val < 0 { lat_val = 0 @@ -110,7 +110,7 @@ pub fn point_to_integers(pt: Point) -> (i64, i64) { lat_val = 2 * LATITUDE_MAX as i64 * LAT_INTEGER_MULTIPLIER - 1; } - let mut lng_val = (lng * LNG_INTEGER_MULTIPLIER as f64).round() as i64; + let mut lng_val = (lng * LNG_INTEGER_MULTIPLIER as f64).floor() as i64; lng_val += LONGITUDE_MAX as i64 * LNG_INTEGER_MULTIPLIER; if lng_val < 0 { lng_val = lng_val % (2 * LONGITUDE_MAX as i64 * LNG_INTEGER_MULTIPLIER) @@ -131,15 +131,15 @@ pub fn point_to_integers(pt: Point) -> (i64, i64) { /// 11 or 12 are probably the limit of useful codes. pub fn encode(pt: Point, code_length: usize) -> String { let (lat_val, lng_val) = point_to_integers(pt); - let trimmed_code_length = min(max(code_length, MIN_CODE_LENGTH), MAX_CODE_LENGTH); - encode_integers(lat_val, lng_val, trimmed_code_length) + encode_integers(lat_val, lng_val, code_length) } /// Encode an integer location into an Open Location Code. /// /// This function is only exposed for testing and should not be called directly. -pub fn encode_integers(mut lat_val: i64, mut lng_val: i64, code_length: usize) -> String { +pub fn encode_integers(mut lat_val: i64, mut lng_val: i64, code_length_raw: usize) -> String { + let code_length = min(max(code_length_raw, MIN_CODE_LENGTH), MAX_CODE_LENGTH); // Compute the code digits. This largely ignores the requested length - it // generates either a 10 digit code, or a 15 digit code, and then truncates // it to the requested length. diff --git a/rust/tests/all_test.rs b/rust/tests/all_test.rs index 0b3938d0..6e35a251 100644 --- a/rust/tests/all_test.rs +++ b/rust/tests/all_test.rs @@ -4,7 +4,10 @@ use std::time::Instant; use csv_reader::CSVReader; use geo::Point; -use open_location_code::{decode, encode, is_full, is_short, is_valid, recover_nearest, shorten}; +use open_location_code::{ + decode, encode, encode_integers, is_full, is_short, is_valid, point_to_integers, + recover_nearest, shorten, +}; use rand::random_range; /// CSVReader is written to swallow errors; as such, we might "pass" tests because we didn't @@ -59,6 +62,9 @@ fn decode_test() { #[test] fn encode_test() { let mut tested = 0; + let mut errors = 0; + // Allow a small proportion of errors due to floating point. + let allowed_error_rate = 0.05; for line in CSVReader::new("encoding.csv") { if line.chars().count() == 0 { continue; @@ -66,11 +72,77 @@ fn encode_test() { let cols: Vec<&str> = line.split(',').collect(); let lat = cols[0].parse::().unwrap(); let lng = cols[1].parse::().unwrap(); - let len = cols[2].parse::().unwrap(); - let code = cols[3]; + let len = cols[4].parse::().unwrap(); + let code = cols[5]; + + let got = encode(Point::new(lng, lat), len); + if got != code { + errors += 1; + println!( + "encode(Point::new({}, {}), {}) want {}, got {}", + lng, lat, len, code, got + ); + } + + tested += 1; + } + assert!( + errors as f32 / tested as f32 <= allowed_error_rate, + "too many encoding errors ({})", + errors + ); + assert!(tested > 0); +} + +#[test] +fn point_to_integers_test() { + let mut tested = 0; + for line in CSVReader::new("encoding.csv") { + if line.chars().count() == 0 { + continue; + } + let cols: Vec<&str> = line.split(',').collect(); + let lat_deg = cols[0].parse::().unwrap(); + let lng_deg = cols[1].parse::().unwrap(); + let lat_int = cols[2].parse::().unwrap(); + let lng_int = cols[3].parse::().unwrap(); + + let (got_lat, got_lng) = point_to_integers(Point::new(lng_deg, lat_deg)); + assert!( + got_lat >= lat_int - 1 && got_lat <= lat_int, + "converting lat={}, want={}, got={}", + lat_deg, + lat_int, + got_lat + ); + assert!( + got_lng >= lng_int - 1 && got_lng <= lng_int, + "converting lng={}, want={}, got={}", + lng_deg, + lng_int, + got_lng + ); + + tested += 1; + } + assert!(tested > 0); +} + +#[test] +fn encode_integers_test() { + let mut tested = 0; + for line in CSVReader::new("encoding.csv") { + if line.chars().count() == 0 { + continue; + } + let cols: Vec<&str> = line.split(',').collect(); + let lat = cols[2].parse::().unwrap(); + let lng = cols[3].parse::().unwrap(); + let len = cols[4].parse::().unwrap(); + let code = cols[5]; assert_eq!( - encode(Point::new(lng, lat), len), + encode_integers(lat, lng, len), code, "encoding lat={},lng={},len={}", lat,