Skip to content

Commit bfd837a

Browse files
authored
Fix floating point tests in Rust implementation (#350)
1 parent 5d2bfc2 commit bfd837a

File tree

3 files changed

+110
-107
lines changed

3 files changed

+110
-107
lines changed

rust/src/consts.rs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,57 @@ pub const CODE_ALPHABET: [char; 20] = [
1515
];
1616

1717
// The base to use to convert numbers to/from.
18-
pub const ENCODING_BASE: f64 = 20f64;
18+
pub const ENCODING_BASE: usize = 20;
1919

2020
// The maximum value for latitude in degrees.
2121
pub const LATITUDE_MAX: f64 = 90f64;
2222

2323
// The maximum value for longitude in degrees.
2424
pub const LONGITUDE_MAX: f64 = 180f64;
2525

26+
// Maximum number of digits to process for plus codes.
27+
pub const MAX_CODE_LENGTH: usize = 15;
28+
2629
// Maximum code length using lat/lng pair encoding. The area of such a
2730
// code is approximately 13x13 meters (at the equator), and should be suitable
2831
// for identifying buildings. This excludes prefix and separator characters.
2932
pub const PAIR_CODE_LENGTH: usize = 10;
3033

31-
// Maximum number of digits to process for plus codes.
32-
pub const MAX_CODE_LENGTH: usize = 15;
34+
// Digits in the grid encoding..
35+
pub const GRID_CODE_LENGTH: usize = 5;
3336

3437
// The resolution values in degrees for each position in the lat/lng pair
3538
// encoding. These give the place value of each position, and therefore the
3639
// dimensions of the resulting area.
3740
pub const PAIR_RESOLUTIONS: [f64; 5] = [20.0f64, 1.0f64, 0.05f64, 0.0025f64, 0.000125f64];
3841

3942
// Number of columns in the grid refinement method.
40-
pub const GRID_COLUMNS: f64 = 4f64;
43+
pub const GRID_COLUMNS: usize = 4;
4144

4245
// Number of rows in the grid refinement method.
43-
pub const GRID_ROWS: f64 = 5f64;
46+
pub const GRID_ROWS: usize = 5;
4447

4548
// Minimum length of a code that can be shortened.
4649
pub const MIN_TRIMMABLE_CODE_LEN: usize = 6;
4750

48-
// Precision of "gravity" to closest larger integer value.
49-
pub const NARROW_REGION_PRECISION: f64 = 1e-9;
51+
// What to multiple latitude degrees by to get an integer value. There are three pairs representing
52+
// decimal digits, and five digits in the grid.
53+
pub const LAT_INTEGER_MULTIPLIER: i64 = (ENCODING_BASE
54+
* ENCODING_BASE
55+
* ENCODING_BASE
56+
* GRID_ROWS
57+
* GRID_ROWS
58+
* GRID_ROWS
59+
* GRID_ROWS
60+
* GRID_ROWS) as i64;
61+
62+
// What to multiple longitude degrees by to get an integer value. There are three pairs representing
63+
// decimal digits, and five digits in the grid.
64+
pub const LNG_INTEGER_MULTIPLIER: i64 = (ENCODING_BASE
65+
* ENCODING_BASE
66+
* ENCODING_BASE
67+
* GRID_COLUMNS
68+
* GRID_COLUMNS
69+
* GRID_COLUMNS
70+
* GRID_COLUMNS
71+
* GRID_COLUMNS) as i64;

rust/src/interface.rs

Lines changed: 77 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
use geo::Point;
2+
use std::cmp;
23

34
use codearea::CodeArea;
45

56
use consts::{
6-
CODE_ALPHABET, ENCODING_BASE, GRID_COLUMNS, GRID_ROWS, LATITUDE_MAX, LONGITUDE_MAX,
7-
MAX_CODE_LENGTH, MIN_TRIMMABLE_CODE_LEN, PADDING_CHAR, PADDING_CHAR_STR, PAIR_CODE_LENGTH,
8-
PAIR_RESOLUTIONS, SEPARATOR, SEPARATOR_POSITION,
7+
CODE_ALPHABET, ENCODING_BASE, GRID_CODE_LENGTH, GRID_COLUMNS, GRID_ROWS, LATITUDE_MAX,
8+
LAT_INTEGER_MULTIPLIER, LNG_INTEGER_MULTIPLIER, LONGITUDE_MAX, MAX_CODE_LENGTH,
9+
MIN_TRIMMABLE_CODE_LEN, PADDING_CHAR, PADDING_CHAR_STR, PAIR_CODE_LENGTH, PAIR_RESOLUTIONS,
10+
SEPARATOR, SEPARATOR_POSITION,
911
};
1012

1113
use private::{
12-
clip_latitude, code_value, compute_latitude_precision, narrow_region, normalize_longitude,
13-
prefix_by_reference,
14+
clip_latitude, code_value, compute_latitude_precision, normalize_longitude, prefix_by_reference,
1415
};
1516

1617
/// Determines if a code is a valid Open Location Code.
@@ -104,48 +105,70 @@ pub fn is_full(_code: &str) -> bool {
104105
/// 11 or 12 are probably the limit of useful codes.
105106
pub fn encode(pt: Point<f64>, code_length: usize) -> String {
106107
let mut lat = clip_latitude(pt.lat());
107-
let mut lng = normalize_longitude(pt.lng());
108+
let lng = normalize_longitude(pt.lng());
108109

109-
let mut trimmed_code_length = code_length;
110-
if trimmed_code_length > MAX_CODE_LENGTH {
111-
trimmed_code_length = MAX_CODE_LENGTH;
112-
}
110+
let trimmed_code_length = cmp::min(code_length, MAX_CODE_LENGTH);
113111

114112
// Latitude 90 needs to be adjusted to be just less, so the returned code
115113
// can also be decoded.
116114
if lat > LATITUDE_MAX || (LATITUDE_MAX - lat) < 1e-10f64 {
117115
lat -= compute_latitude_precision(trimmed_code_length);
118116
}
119117

120-
lat += LATITUDE_MAX;
121-
lng += LONGITUDE_MAX;
118+
// Convert to integers.
119+
let mut lat_val =
120+
(((lat + LATITUDE_MAX) * LAT_INTEGER_MULTIPLIER as f64 * 1e6).round() / 1e6f64) as i64;
121+
let mut lng_val =
122+
(((lng + LONGITUDE_MAX) * LNG_INTEGER_MULTIPLIER as f64 * 1e6).round() / 1e6f64) as i64;
122123

123-
let mut code = String::with_capacity(trimmed_code_length + 1);
124-
let mut digit = 0;
125-
while digit < trimmed_code_length {
126-
narrow_region(digit, &mut lat, &mut lng);
124+
// Compute the code digits. This largely ignores the requested length - it
125+
// generates either a 10 digit code, or a 15 digit code, and then truncates
126+
// it to the requested length.
127127

128-
let lat_digit = lat as usize;
129-
let lng_digit = lng as usize;
130-
if digit < PAIR_CODE_LENGTH {
131-
code.push(CODE_ALPHABET[lat_digit]);
132-
code.push(CODE_ALPHABET[lng_digit]);
133-
digit += 2;
134-
} else {
135-
code.push(CODE_ALPHABET[4 * lat_digit + lng_digit]);
136-
digit += 1;
128+
// Build up the code digits in reverse order.
129+
let mut rev_code = String::with_capacity(trimmed_code_length + 1);
130+
131+
// First do the grid digits.
132+
if code_length > PAIR_CODE_LENGTH {
133+
for _i in 0..GRID_CODE_LENGTH {
134+
let lat_digit = lat_val % GRID_ROWS as i64;
135+
let lng_digit = lng_val % GRID_COLUMNS as i64;
136+
let ndx = (lat_digit * GRID_COLUMNS as i64 + lng_digit) as usize;
137+
rev_code.push(CODE_ALPHABET[ndx]);
138+
lat_val /= GRID_ROWS as i64;
139+
lng_val /= GRID_COLUMNS as i64;
137140
}
138-
lat -= lat_digit as f64;
139-
lng -= lng_digit as f64;
140-
if digit == SEPARATOR_POSITION {
141-
code.push(SEPARATOR);
141+
} else {
142+
// Adjust latitude and longitude values to skip the grid digits.
143+
lat_val /= GRID_ROWS.pow(GRID_CODE_LENGTH as u32) as i64;
144+
lng_val /= GRID_COLUMNS.pow(GRID_CODE_LENGTH as u32) as i64;
145+
}
146+
// Compute the pair section of the code.
147+
for i in 0..PAIR_CODE_LENGTH / 2 {
148+
rev_code.push(CODE_ALPHABET[(lng_val % ENCODING_BASE as i64) as usize]);
149+
lng_val /= ENCODING_BASE as i64;
150+
rev_code.push(CODE_ALPHABET[(lat_val % ENCODING_BASE as i64) as usize]);
151+
lat_val /= ENCODING_BASE as i64;
152+
// If we are at the separator position, add the separator.
153+
if i == 0 {
154+
rev_code.push(SEPARATOR);
142155
}
143156
}
144-
if digit < SEPARATOR_POSITION {
145-
code.push_str(PADDING_CHAR_STR.repeat(SEPARATOR_POSITION - digit).as_str());
157+
let mut code: String;
158+
// If we need to pad the code, replace some of the digits.
159+
if code_length < SEPARATOR_POSITION {
160+
code = rev_code.chars().rev().take(code_length).collect();
161+
code.push_str(
162+
PADDING_CHAR_STR
163+
.repeat(SEPARATOR_POSITION - code_length)
164+
.as_str(),
165+
);
146166
code.push(SEPARATOR);
167+
} else {
168+
code = rev_code.chars().rev().take(code_length + 1).collect();
147169
}
148-
code
170+
171+
return code;
149172
}
150173

151174
/// Decodes an Open Location Code into the location coordinates.
@@ -165,35 +188,36 @@ pub fn decode(_code: &str) -> Result<CodeArea, String> {
165188
code = code.chars().take(MAX_CODE_LENGTH).collect();
166189
}
167190

168-
let mut lat = -LATITUDE_MAX;
169-
let mut lng = -LONGITUDE_MAX;
170-
let mut lat_res = ENCODING_BASE * ENCODING_BASE;
171-
let mut lng_res = ENCODING_BASE * ENCODING_BASE;
191+
// Work out the values as integers and convert to floating point at the end.
192+
let mut lat: i64 = -90 * LAT_INTEGER_MULTIPLIER;
193+
let mut lng: i64 = -180 * LNG_INTEGER_MULTIPLIER;
194+
let mut lat_place_val: i64 = LAT_INTEGER_MULTIPLIER * ENCODING_BASE.pow(2) as i64;
195+
let mut lng_place_val: i64 = LNG_INTEGER_MULTIPLIER * ENCODING_BASE.pow(2) as i64;
172196

173197
for (idx, chr) in code.chars().enumerate() {
174198
if idx < PAIR_CODE_LENGTH {
175199
if idx % 2 == 0 {
176-
lat_res /= ENCODING_BASE;
177-
lat += lat_res * code_value(chr) as f64;
200+
lat_place_val /= ENCODING_BASE as i64;
201+
lat += lat_place_val * code_value(chr) as i64;
178202
} else {
179-
lng_res /= ENCODING_BASE;
180-
lng += lng_res * code_value(chr) as f64;
203+
lng_place_val /= ENCODING_BASE as i64;
204+
lng += lng_place_val * code_value(chr) as i64;
181205
}
182-
} else if idx < MAX_CODE_LENGTH {
183-
lat_res /= GRID_ROWS;
184-
lng_res /= GRID_COLUMNS;
185-
lat += lat_res * (code_value(chr) as f64 / GRID_COLUMNS).trunc();
186-
187-
lng += lng_res * (code_value(chr) as f64 % GRID_COLUMNS);
206+
} else {
207+
lat_place_val /= GRID_ROWS as i64;
208+
lng_place_val /= GRID_COLUMNS as i64;
209+
lat += lat_place_val * (code_value(chr) / GRID_COLUMNS) as i64;
210+
lng += lng_place_val * (code_value(chr) % GRID_COLUMNS) as i64;
188211
}
189212
}
190-
Ok(CodeArea::new(
191-
lat,
192-
lng,
193-
lat + lat_res,
194-
lng + lng_res,
195-
code.len(),
196-
))
213+
// Convert to floating point values.
214+
let lat_lo: f64 = lat as f64 / LAT_INTEGER_MULTIPLIER as f64;
215+
let lng_lo: f64 = lng as f64 / LNG_INTEGER_MULTIPLIER as f64;
216+
let lat_hi: f64 =
217+
(lat + lat_place_val) as f64 / (ENCODING_BASE.pow(3) * GRID_ROWS.pow(5)) as f64;
218+
let lng_hi: f64 =
219+
(lng + lng_place_val) as f64 / (ENCODING_BASE.pow(3) * GRID_COLUMNS.pow(5)) as f64;
220+
Ok(CodeArea::new(lat_lo, lng_lo, lat_hi, lng_hi, code.len()))
197221
}
198222

199223
/// Remove characters from the start of an OLC code.

rust/src/private.rs

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use consts::{
2-
CODE_ALPHABET, ENCODING_BASE, GRID_COLUMNS, GRID_ROWS, LATITUDE_MAX, LONGITUDE_MAX,
3-
NARROW_REGION_PRECISION, PAIR_CODE_LENGTH,
2+
CODE_ALPHABET, ENCODING_BASE, GRID_ROWS, LATITUDE_MAX, LONGITUDE_MAX, PAIR_CODE_LENGTH,
43
};
54

65
use interface::encode;
@@ -31,9 +30,10 @@ pub fn clip_latitude(latitude_degrees: f64) -> f64 {
3130

3231
pub fn compute_latitude_precision(code_length: usize) -> f64 {
3332
if code_length <= PAIR_CODE_LENGTH {
34-
return ENCODING_BASE.powf((code_length as f64 / -2f64 + 2f64).floor());
33+
return (ENCODING_BASE as f64).powf((code_length as f64 / -2f64 + 2f64).floor());
3534
}
36-
ENCODING_BASE.powi(-3i32) / GRID_ROWS.powf(code_length as f64 - PAIR_CODE_LENGTH as f64)
35+
(ENCODING_BASE as f64).powf(-3f64)
36+
/ GRID_ROWS.pow((code_length - PAIR_CODE_LENGTH) as u32) as f64
3737
}
3838

3939
pub fn prefix_by_reference(pt: Point<f64>, code_length: usize) -> String {
@@ -48,46 +48,3 @@ pub fn prefix_by_reference(pt: Point<f64>, code_length: usize) -> String {
4848
code.drain(code_length..);
4949
code
5050
}
51-
52-
// Apply "gravity" towards closest integer value, if current value is closer than given threshold.
53-
// This is a way to compensate aggregated error caused by floating point precision restriction.
54-
fn near(value: f64, error: f64) -> f64 {
55-
let target = (value + error).trunc();
56-
if value.trunc() != target {
57-
target
58-
} else {
59-
value
60-
}
61-
}
62-
63-
pub fn narrow_region(digit: usize, lat: &mut f64, lng: &mut f64) {
64-
if digit == 0 {
65-
*lat /= ENCODING_BASE;
66-
*lng /= ENCODING_BASE;
67-
} else if digit < PAIR_CODE_LENGTH {
68-
*lat *= ENCODING_BASE;
69-
*lng *= ENCODING_BASE;
70-
} else {
71-
*lat *= GRID_ROWS;
72-
*lng *= GRID_COLUMNS
73-
}
74-
*lat = near(*lat, NARROW_REGION_PRECISION);
75-
*lng = near(*lng, NARROW_REGION_PRECISION);
76-
}
77-
78-
#[cfg(test)]
79-
mod tests {
80-
use super::*;
81-
82-
#[test]
83-
fn near_applied() {
84-
let value = 3.0f64 - NARROW_REGION_PRECISION * 2.;
85-
assert_eq!(near(value, NARROW_REGION_PRECISION), value);
86-
}
87-
88-
#[test]
89-
fn near_not_applied() {
90-
let value = 3.0f64 - NARROW_REGION_PRECISION;
91-
assert_eq!(near(value, NARROW_REGION_PRECISION), 3.0f64);
92-
}
93-
}

0 commit comments

Comments
 (0)