Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions c/openlocationcode_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,8 @@ TEST_P(EncodingChecks, OLC_LocationToIntegers) {
OLC_LatLon loc = OLC_LatLon{test_data.lat_deg, test_data.lng_deg};
OLC_LatLonIntegers got;
OLC_LocationToIntegers(&loc, &got);
// Due to floating point precision limitations, we may get values 1 less than
// expected.
EXPECT_LE(got.lat, test_data.lat_int);
EXPECT_GE(got.lat + 1, test_data.lat_int);
EXPECT_LE(got.lon, test_data.lng_int);
EXPECT_GE(got.lon + 1, test_data.lng_int);
EXPECT_EQ(test_data.lat_int, got.lat);
EXPECT_EQ(test_data.lng_int, got.lon);
}

INSTANTIATE_TEST_SUITE_P(OLC_Tests, EncodingChecks,
Expand Down
22 changes: 17 additions & 5 deletions c/src/olc.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,25 @@ int OLC_IsFull(const char* code, size_t size) {
return is_full(&info);
}

// Apply floor after multiplying by precision, with correction for
// floating-point errors. Due to floating-point representation, multiplying
// a value like 129.7 by 8192000 may produce 1062502399.9999999 instead of the
// exact 1062502400. A simple floor() would incorrectly return 1062502399.
// This function detects and corrects such cases.
static long long int corrected_floor(double value, long long int precision) {
long long int n = (long long int)floorl(value * (double)precision);
// Check if (n + 1) / precision <= value. If so, n + 1 is the correct floor.
if ((double)(n + 1) / (double)precision <= value) {
return n + 1;
}
return n;
}

void OLC_LocationToIntegers(const OLC_LatLon* degrees,
OLC_LatLonIntegers* integers) {
// Multiply degrees by precision. Use lround to explicitly round rather than
// truncate, which causes issues when using values like 0.1 that do not have
// precise floating point representations.
long long int lat = floorl(degrees->lat * kGridLatPrecisionInverse);
long long int lon = floorl(degrees->lon * kGridLonPrecisionInverse);
// Multiply degrees by precision with correction for floating-point errors.
long long int lat = corrected_floor(degrees->lat, kGridLatPrecisionInverse);
long long int lon = corrected_floor(degrees->lon, kGridLonPrecisionInverse);

// Convert latitude to positive range (0..2*degrees*precision) and clip.
lat += OLC_kLatMaxDegrees * kGridLatPrecisionInverse;
Expand Down
18 changes: 16 additions & 2 deletions cpp/openlocationcode.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,22 @@ const int kPositionLUT['X' - 'C' + 1] = {8, -1, -1, 9, 10, 11, -1, 12,
-1, -1, 13, -1, -1, 14, 15, 16,
-1, -1, -1, 17, 18, 19};

// Apply floor after multiplying by precision, with correction for
// floating-point errors. Due to floating-point representation, multiplying
// a value like 129.7 by 8192000 may produce 1062502399.9999999 instead of the
// exact 1062502400. A simple floor() would incorrectly return 1062502399.
// This function detects and corrects such cases.
int64_t correctedFloor(double value, int64_t precision) {
int64_t n = static_cast<int64_t>(floor(value * static_cast<double>(precision)));
// Check if (n + 1) / precision <= value. If so, n + 1 is the correct floor.
if (static_cast<double>(n + 1) / static_cast<double>(precision) <= value) {
return n + 1;
}
return n;
}

int64_t latitudeToInteger(double latitude) {
int64_t lat = floor(latitude * kGridLatPrecisionInverse);
int64_t lat = correctedFloor(latitude, kGridLatPrecisionInverse);
lat += kLatitudeMaxDegrees * kGridLatPrecisionInverse;
if (lat < 0) {
lat = 0;
Expand All @@ -60,7 +74,7 @@ int64_t latitudeToInteger(double latitude) {
}

int64_t longitudeToInteger(double longitude) {
int64_t lng = floor(longitude * kGridLngPrecisionInverse);
int64_t lng = correctedFloor(longitude, kGridLngPrecisionInverse);
lng += kLongitudeMaxDegrees * kGridLngPrecisionInverse;
if (lng <= 0) {
lng = lng % (2 * kLongitudeMaxDegrees * kGridLngPrecisionInverse) +
Expand Down
8 changes: 2 additions & 6 deletions cpp/openlocationcode_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,9 @@ TEST_P(EncodingChecks, OLC_EncodeIntegers) {
TEST_P(EncodingChecks, OLC_LocationToIntegers) {
EncodingTestData test_data = GetParam();
int64_t got_lat = internal::latitudeToInteger(test_data.lat_deg);
// Due to floating point precision limitations, we may get values 1 less than
// expected.
EXPECT_LE(got_lat, test_data.lat_int);
EXPECT_GE(got_lat + 1, test_data.lat_int);
EXPECT_EQ(test_data.lat_int, got_lat);
int64_t got_lng = internal::longitudeToInteger(test_data.lng_deg);
EXPECT_LE(got_lng, test_data.lng_int);
EXPECT_GE(got_lng + 1, test_data.lng_int);
EXPECT_EQ(test_data.lng_int, got_lng);
}

INSTANTIATE_TEST_CASE_P(OLC_Tests, EncodingChecks,
Expand Down
18 changes: 16 additions & 2 deletions dart/lib/src/open_location_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,27 @@ String encode(num latitude, num longitude, {int codeLength = pairCodeLength}) {
return encodeIntegers(integers[0], integers[1], codeLength);
}

// Apply floor after multiplying by precision, with correction for
// floating-point errors. Due to floating-point representation, multiplying
// a value like 129.7 by 8192000 may produce 1062502399.9999999 instead of
// the exact 1062502400. A simple floor() would incorrectly return 1062502399.
// This function detects and corrects such cases.
int _correctedFloor(num value, int precision) {
var n = (value * precision).floor().toInt();
// Check if (n + 1) / precision <= value. If so, n + 1 is the correct floor.
if ((n + 1) / precision <= value) {
return n + 1;
}
return n;
}

// Convert latitude and longitude in degrees to the integer values needed for
// reliable conversions.
List<int> locationToIntegers(num latitude, num longitude) {
// Convert latitude into a positive integer clipped into the range 0-(just
// under 180*2.5e7). Latitude 90 needs to be adjusted to be just less, so the
// returned code can also be decoded.
var latVal = (latitude * finalLatPrecision).floor().toInt();
var latVal = _correctedFloor(latitude, finalLatPrecision);
latVal += latitudeMax * finalLatPrecision;
if (latVal < 0) {
latVal = 0;
Expand All @@ -273,7 +287,7 @@ List<int> locationToIntegers(num latitude, num longitude) {
}
// Convert longitude into a positive integer and normalise it into the range
// 0-360*8.192e6.
var lngVal = (longitude * finalLngPrecision).floor().toInt();
var lngVal = _correctedFloor(longitude, finalLngPrecision);
lngVal += longitudeMax * finalLngPrecision;
if (lngVal < 0) {
// Dart's % operator differs from other languages in that it returns the
Expand Down
21 changes: 3 additions & 18 deletions dart/test/encode_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,36 +46,21 @@ void checkLocationToIntegers(String csvLine) {
int latInteger = int.parse(elements[2]);
int lngInteger = int.parse(elements[3]);
var got = olc.locationToIntegers(latDegrees, lngDegrees);
// Due to floating point precision limitations, we may get values 1 less than expected.
expect(got[0], lessThanOrEqualTo(latInteger));
expect(got[0] + 1, greaterThanOrEqualTo(latInteger));
expect(got[1], lessThanOrEqualTo(lngInteger));
expect(got[1] + 1, greaterThanOrEqualTo(lngInteger));
expect(got[0], equals(latInteger));
expect(got[1], equals(lngInteger));
}

void main() {
// Encoding from degrees permits a small percentage of errors.
// This is due to floating point precision limitations.
test('Check encode from degrees', () {
// The proportion of tests that we will accept generating a different code.
// This should not be significantly different from any other implementation.
num allowedErrRate = 0.05;
int errors = 0;
int tests = 0;
csvLinesFromFile('encoding.csv').forEach((csvLine) {
tests++;
var elements = csvLine.split(',');
num lat = double.parse(elements[0]);
num lng = double.parse(elements[1]);
int len = int.parse(elements[4]);
var want = elements[5];
var got = olc.encode(lat, lng, codeLength: len);
if (got != want) {
print("ENCODING DIFFERENCE: Got '$got', expected '$want'");
errors++;
}
expect(got, equals(want));
});
expect(errors / tests, lessThanOrEqualTo(allowedErrRate));
});

test('Check encode from integers', () {
Expand Down
18 changes: 16 additions & 2 deletions go/olc.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,24 @@ func normalizeLng(value float64) float64 {
return normalize(value, lngMax)
}

// correctedFloor applies floor after multiplying by precision, with correction
// for floating-point errors. Due to floating-point representation, multiplying
// a value like 129.7 by 8192000 may produce 1062502399.9999999 instead of the
// exact 1062502400. A simple floor() would incorrectly return 1062502399.
// This function detects and corrects such cases.
func correctedFloor(value float64, precision int64) int64 {
n := int64(math.Floor(value * float64(precision)))
// Check if (n + 1) / precision <= value. If so, n + 1 is the correct floor.
if float64(n+1)/float64(precision) <= value {
return n + 1
}
return n
}

// latitudeAsInteger converts a latitude in degrees into the integer representation.
// It will be clipped into the degree range -90<=x<90 (actually 0-180*2.5e7-1).
func latitudeAsInteger(latDegrees float64) int64 {
latVal := int64(math.Floor(latDegrees * finalLatPrecision))
latVal := correctedFloor(latDegrees, finalLatPrecision)
latVal += latMax * finalLatPrecision
if latVal < 0 {
latVal = 0
Expand All @@ -245,7 +259,7 @@ func latitudeAsInteger(latDegrees float64) int64 {
// longitudeAsInteger converts a longitude in degrees into the integer representation.
// It will be normalised into the degree range -180<=x<180 (actually 0-360*8.192e6).
func longitudeAsInteger(lngDegrees float64) int64 {
lngVal := int64(math.Floor(lngDegrees * finalLngPrecision))
lngVal := correctedFloor(lngDegrees, finalLngPrecision)
lngVal += lngMax * finalLngPrecision
if lngVal <= 0 {
lngVal = lngVal%(2*lngMax*finalLngPrecision) + 2*lngMax*finalLngPrecision
Expand Down
13 changes: 3 additions & 10 deletions go/olc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package olc
import (
"bufio"
"encoding/csv"
"fmt"
"math"
"math/rand"
"os"
Expand Down Expand Up @@ -132,18 +131,12 @@ func TestCheck(t *testing.T) {
}

func TestEncodeDegrees(t *testing.T) {
const allowedErrRate float64 = 0.05
var badCodes int
for i, elt := range encoding {
got := Encode(elt.latDeg, elt.lngDeg, elt.length)
if got != elt.code {
fmt.Printf("ENCODING DIFFERENCE %d. got %q for Encode(%v,%v,%d), wanted %q\n", i, got, elt.latDeg, elt.lngDeg, elt.length, elt.code)
badCodes++
t.Errorf("%d. got %q for Encode(%v,%v,%d), wanted %q", i, got, elt.latDeg, elt.lngDeg, elt.length, elt.code)
}
}
if errRate := float64(badCodes) / float64(len(encoding)); errRate > allowedErrRate {
t.Errorf("Too many errors in encoding degrees (got %f, allowed %f)", errRate, allowedErrRate)
}
}

func TestEncodeIntegers(t *testing.T) {
Expand All @@ -158,11 +151,11 @@ func TestEncodeIntegers(t *testing.T) {
func TestConvertDegrees(t *testing.T) {
for i, elt := range encoding {
got := latitudeAsInteger(elt.latDeg)
if got > elt.latInt || got < elt.latInt-1 {
if got != elt.latInt {
t.Errorf("%d. got %d for latitudeAsInteger(%v), wanted %d", i, got, elt.latDeg, elt.latInt)
}
got = longitudeAsInteger(elt.lngDeg)
if got > elt.lngInt || got < elt.lngInt-1 {
if got != elt.lngInt {
t.Errorf("%d. got %d for longitudeAsInteger(%v), wanted %d", i, got, elt.lngDeg, elt.lngInt)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,26 @@ public static boolean isShortCode(String code) {

// Private static methods.

/**
* Apply floor after multiplying by precision, with correction for floating-point errors.
*
* <p>Due to floating-point representation, multiplying a value like 129.7 by 8192000 may produce
* 1062502399.9999999 instead of the exact 1062502400. A simple floor() would incorrectly return
* 1062502399. This function detects and corrects such cases.
*
* @param value The value to multiply and floor.
* @param precision The precision multiplier.
* @return The corrected floor result.
*/
private static long correctedFloor(double value, long precision) {
long n = (long) Math.floor(value * (double) precision);
// Check if (n + 1) / precision <= value. If so, n + 1 is the correct floor.
if ((double) (n + 1) / (double) precision <= value) {
return n + 1;
}
return n;
}

/**
* Convert latitude and longitude in degrees into the integer values needed for reliable encoding.
* (To avoid floating point precision errors.)
Expand All @@ -665,8 +685,8 @@ public static boolean isShortCode(String code) {
* @return A list of [latitude, longitude] in clipped, normalised integer values.
*/
static long[] degreesToIntegers(double latitude, double longitude) {
long lat = (long) Math.floor(latitude * LAT_INTEGER_MULTIPLIER);
long lng = (long) Math.floor(longitude * LNG_INTEGER_MULTIPLIER);
long lat = correctedFloor(latitude, LAT_INTEGER_MULTIPLIER);
long lng = correctedFloor(longitude, LNG_INTEGER_MULTIPLIER);

// Clip and normalise values.
lat += LATITUDE_MAX * LAT_INTEGER_MULTIPLIER;
Expand Down
51 changes: 22 additions & 29 deletions java/src/test/java/com/google/openlocationcode/EncodingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,53 +61,46 @@ public void setUp() throws Exception {

@Test
public void testEncodeFromDegrees() {
double allowedErrorRate = 0.05;
int failedEncodings = 0;
for (TestData testData : testDataList) {
String got =
OpenLocationCode.encode(
testData.latitudeDegrees, testData.longitudeDegrees, testData.length);
if (!testData.code.equals(got)) {
failedEncodings++;
System.out.printf(
"ENCODING DIFFERENCE: encode(%f,%f,%d) got %s, want %s\n",
testData.latitudeDegrees,
testData.longitudeDegrees,
testData.length,
got,
testData.code);
}
Assert.assertEquals(
String.format(
"encode(%f,%f,%d) want %s, got %s",
testData.latitudeDegrees,
testData.longitudeDegrees,
testData.length,
testData.code,
got),
testData.code,
got);
}
double gotRate = (double) failedEncodings / (double) testDataList.size();
Assert.assertTrue(
String.format(
"Too many encoding errors (actual rate %f, allowed rate %f), see ENCODING DIFFERENCE"
+ " lines",
gotRate, allowedErrorRate),
gotRate <= allowedErrorRate);
}

@Test
public void testDegreesToIntegers() {
for (TestData testData : testDataList) {
long[] got =
OpenLocationCode.degreesToIntegers(testData.latitudeDegrees, testData.longitudeDegrees);
Assert.assertTrue(
Assert.assertEquals(
String.format(
"degreesToIntegers(%f, %f) returned latitude %d, expected %d",
"degreesToIntegers(%f, %f) latitude: want %d, got %d",
testData.latitudeDegrees,
testData.longitudeDegrees,
got[0],
testData.latitudeInteger),
got[0] == testData.latitudeInteger || got[0] == testData.latitudeInteger - 1);
Assert.assertTrue(
testData.latitudeInteger,
got[0]),
testData.latitudeInteger,
got[0]);
Assert.assertEquals(
String.format(
"degreesToIntegers(%f, %f) returned longitude %d, expected %d",
"degreesToIntegers(%f, %f) longitude: want %d, got %d",
testData.latitudeDegrees,
testData.longitudeDegrees,
got[1],
testData.longitudeInteger),
got[1] == testData.longitudeInteger || got[1] == testData.longitudeInteger - 1);
testData.longitudeInteger,
got[1]),
testData.longitudeInteger,
got[1]);
}
}

Expand Down
Loading