Skip to content

Commit cff6d0b

Browse files
authored
Refactor golang encode and decode (#703)
1 parent 73e9a50 commit cff6d0b

3 files changed

Lines changed: 86 additions & 110 deletions

File tree

go/decode.go

Lines changed: 44 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -16,86 +16,71 @@ package olc
1616

1717
import (
1818
"errors"
19-
"math"
2019
"strings"
2120
)
2221

2322
// Decode decodes an Open Location Code into the location coordinates.
2423
// Returns a CodeArea object that includes the coordinates of the bounding
2524
// box - the lower left, center and upper right.
2625
//
27-
// To avoid underflow errors, the precision is limited to 15 digits.
2826
// Longer codes are allowed, but only the first 15 is decoded.
2927
func Decode(code string) (CodeArea, error) {
3028
var area CodeArea
3129
if err := CheckFull(code); err != nil {
3230
return area, err
3331
}
34-
// Strip out separator character (we've already established the code is
35-
// valid so the maximum is one), padding characters and convert to upper
32+
// Strip out separator character, padding characters and convert to upper
3633
// case.
3734
code = StripCode(code)
38-
if len(code) < 2 {
35+
codeLen := len(code)
36+
if codeLen < 2 {
3937
return area, errors.New("code too short")
4038
}
41-
// Initialise the values for each section. We work them out as integers and
42-
// convert them to floats at the end.
43-
normalLat := -latMax * pairPrecision
44-
normalLng := -lngMax * pairPrecision
45-
extraLat := 0
46-
extraLng := 0
47-
// How many digits do we have to process?
48-
digits := pairCodeLen
49-
if len(code) < digits {
50-
digits = len(code)
51-
}
52-
// Define the place value for the most significant pair.
53-
pv := pairFPV
54-
for i := 0; i < digits-1; i += 2 {
55-
normalLat += strings.IndexByte(Alphabet, code[i]) * pv
56-
normalLng += strings.IndexByte(Alphabet, code[i+1]) * pv
57-
if i < digits-2 {
58-
pv /= encBase
39+
// lat and lng build up the integer values.
40+
var lat int64
41+
var lng int64
42+
// height and width build up integer values for the height and width of the
43+
// code area. They get set to 1 for the last digit and then multiplied by
44+
// each remaining place.
45+
var height int64 = 1
46+
var width int64 = 1
47+
// Decode the paired digits.
48+
for i := 0; i < pairCodeLen; i += 2 {
49+
lat *= encBase
50+
lng *= encBase
51+
height *= encBase
52+
if i < codeLen {
53+
lat += int64(strings.IndexByte(Alphabet, code[i]))
54+
lng += int64(strings.IndexByte(Alphabet, code[i+1]))
55+
height = 1
5956
}
6057
}
61-
// Convert the place value to a float in degrees.
62-
latPrecision := float64(pv) / pairPrecision
63-
lngPrecision := float64(pv) / pairPrecision
64-
// Process any extra precision digits.
65-
if len(code) > pairCodeLen {
66-
// Initialise the place values for the grid.
67-
rowpv := gridLatFPV
68-
colpv := gridLngFPV
69-
// How many digits do we have to process?
70-
digits = maxCodeLen
71-
if len(code) < maxCodeLen {
72-
digits = len(code)
73-
}
74-
for i := pairCodeLen; i < digits; i++ {
75-
dval := strings.IndexByte(Alphabet, code[i])
76-
row := dval / gridCols
77-
col := dval % gridCols
78-
extraLat += row * rowpv
79-
extraLng += col * colpv
80-
if i < digits-1 {
81-
rowpv /= gridRows
82-
colpv /= gridCols
83-
}
58+
// The paired section has the same resolution for height and width.
59+
width = height
60+
// Decode the grid section.
61+
for i := pairCodeLen; i < maxCodeLen; i++ {
62+
lat *= gridRows
63+
height *= gridRows
64+
lng *= gridCols
65+
width *= gridCols
66+
if i < codeLen {
67+
dval := int64(strings.IndexByte(Alphabet, code[i]))
68+
lat += dval / gridCols
69+
lng += dval % gridCols
70+
height = 1
71+
width = 1
8472
}
85-
// Adjust the precisions from the integer values to degrees.
86-
latPrecision = float64(rowpv) / finalLatPrecision
87-
lngPrecision = float64(colpv) / finalLngPrecision
8873
}
89-
// Merge the values from the normal and extra precision parts of the code.
90-
// Everything is ints so they all need to be cast to floats.
91-
lat := float64(normalLat)/pairPrecision + float64(extraLat)/finalLatPrecision
92-
lng := float64(normalLng)/pairPrecision + float64(extraLng)/finalLngPrecision
93-
// Round everything off to 14 places.
74+
// Convert everything into degrees and return the code area.
75+
var latDegrees float64 = float64(lat-latMax*finalLatPrecision) / float64(finalLatPrecision)
76+
var lngDegrees float64 = float64(lng-lngMax*finalLngPrecision) / float64(finalLngPrecision)
77+
var heightDegrees float64 = float64(height) / float64(finalLatPrecision)
78+
var widthDegrees float64 = float64(width) / float64(finalLngPrecision)
9479
return CodeArea{
95-
LatLo: math.Round(lat*1e14) / 1e14,
96-
LngLo: math.Round(lng*1e14) / 1e14,
97-
LatHi: math.Round((lat+latPrecision)*1e14) / 1e14,
98-
LngHi: math.Round((lng+lngPrecision)*1e14) / 1e14,
99-
Len: len(code),
80+
LatLo: latDegrees,
81+
LngLo: lngDegrees,
82+
LatHi: latDegrees + heightDegrees,
83+
LngHi: lngDegrees + widthDegrees,
84+
Len: codeLen,
10085
}, nil
10186
}

go/encode.go

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package olc
1616

1717
import (
1818
"errors"
19-
"math"
2019
"strings"
2120
)
2221

@@ -42,24 +41,8 @@ var (
4241
func Encode(lat, lng float64, codeLen int) string {
4342
// This approach converts each value to an integer after multiplying it by the final precision.
4443
// This allows us to use only integer operations, so avoiding any accumulation of floating point representation errors.
45-
46-
// Convert latitude into a positive integer clipped into the range 0-(just under 180*2.5e7).
47-
// Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded.
48-
latVal := int64(math.Round(lat * finalLatPrecision))
49-
latVal += latMax * finalLatPrecision
50-
if latVal < 0 {
51-
latVal = 0
52-
} else if latVal >= 2*latMax*finalLatPrecision {
53-
latVal = 2*latMax*finalLatPrecision - 1
54-
}
55-
// Convert longitude into a positive integer and normalise it into the range 0-360*8.192e6.
56-
lngVal := int64(math.Round(lng * finalLngPrecision))
57-
lngVal += lngMax * finalLngPrecision
58-
if lngVal <= 0 {
59-
lngVal = lngVal%(2*lngMax*finalLngPrecision) + 2*lngMax*finalLngPrecision
60-
} else if lngVal >= 2*lngMax*finalLngPrecision {
61-
lngVal = lngVal % (2 * lngMax * finalLngPrecision)
62-
}
44+
latVal := latitudeAsInteger(lat)
45+
lngVal := longitudeAsInteger(lng)
6346

6447
// Clip the code length to legal values.
6548
codeLen = clipCodeLen(codeLen)
@@ -69,39 +52,27 @@ func Encode(lat, lng float64, codeLen int) string {
6952

7053
// Compute the grid part of the code if necessary.
7154
if codeLen > pairCodeLen {
72-
code[sepPos+7], latVal, lngVal = latLngGridStep(latVal, lngVal)
73-
code[sepPos+6], latVal, lngVal = latLngGridStep(latVal, lngVal)
74-
code[sepPos+5], latVal, lngVal = latLngGridStep(latVal, lngVal)
75-
code[sepPos+4], latVal, lngVal = latLngGridStep(latVal, lngVal)
76-
code[sepPos+3], latVal, lngVal = latLngGridStep(latVal, lngVal)
55+
for i := maxCodeLen - pairCodeLen; i >= 1; i-- {
56+
code[sepPos+2+i], latVal, lngVal = latLngGridStep(latVal, lngVal)
57+
}
7758
} else {
7859
latVal /= gridLatFullValue
7960
lngVal /= gridLngFullValue
8061
}
8162

8263
// Add the pair after the separator.
83-
latNdx := latVal % int64(encBase)
84-
lngNdx := lngVal % int64(encBase)
85-
code[sepPos+2] = Alphabet[lngNdx]
86-
code[sepPos+1] = Alphabet[latNdx]
64+
code[sepPos+2], lngVal = pairIndexStep(lngVal)
65+
code[sepPos+1], latVal = pairIndexStep(latVal)
8766

8867
// Avoid the need for string concatenation by filling in the Separator manually.
8968
code[sepPos] = Separator
9069

9170
// Compute the pair section of the code.
9271
// Even indices contain latitude and odd contain longitude.
93-
code[7], lngVal = pairIndexStep(lngVal)
94-
code[6], latVal = pairIndexStep(latVal)
95-
96-
code[5], lngVal = pairIndexStep(lngVal)
97-
code[4], latVal = pairIndexStep(latVal)
98-
99-
code[3], lngVal = pairIndexStep(lngVal)
100-
code[2], latVal = pairIndexStep(latVal)
101-
102-
code[1], _ = pairIndexStep(lngVal)
103-
code[0], _ = pairIndexStep(latVal)
104-
72+
for pairStart := (pairCodeLen/2 + 1); pairStart >= 0; pairStart = pairStart - 2 {
73+
code[pairStart+1], lngVal = pairIndexStep(lngVal)
74+
code[pairStart], latVal = pairIndexStep(latVal)
75+
}
10576
// If we don't need to pad the code, return the requested section.
10677
if codeLen >= sepPos {
10778
return string(code[:codeLen+1])
@@ -139,7 +110,7 @@ func latLngGridStep(latVal, lngVal int64) (byte, int64, int64) {
139110
// pairIndexStep computes the next smallest pair code in sequence,
140111
// followed by the remaining integer not yet converted to a pair code.
141112
func pairIndexStep(coordinate int64) (byte, int64) {
142-
coordinate /= int64(encBase)
143113
latNdx := coordinate % int64(encBase)
114+
coordinate /= int64(encBase)
144115
return Alphabet[latNdx], coordinate
145116
}

go/olc.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,20 @@ const (
3838
Padding = '0'
3939

4040
// Alphabet is the set of valid encoding characters.
41-
Alphabet = "23456789CFGHJMPQRVWX"
42-
encBase = len(Alphabet)
41+
Alphabet = "23456789CFGHJMPQRVWX"
42+
encBase int64 = int64(len(Alphabet))
4343

4444
maxCodeLen = 15
4545
pairCodeLen = 10
4646
gridCodeLen = maxCodeLen - pairCodeLen
4747
gridCols = 4
4848
gridRows = 5
49-
// First place value of the pairs (if the last pair value is 1). encBase^(pairs-1)
50-
pairFPV = 160000
5149
// Precision of the pair part of the code, in 1/degrees.
5250
pairPrecision = 8000
5351
// Full value of the latitude grid - gridRows**gridCodeLen.
5452
gridLatFullValue = 3125
5553
// Full value of the longitude grid - gridCols**gridCodeLen.
5654
gridLngFullValue = 1024
57-
// First place value of the latitude grid (if the last place is 1). gridRows^(gridCodeLen - 1)
58-
gridLatFPV = gridLatFullValue / gridRows
59-
// First place value of the longitude grid (if the last place is 1). gridCols^(gridCodeLen - 1)
60-
gridLngFPV = gridLngFullValue / gridCols
6155
// Latitude precision of a full length code. pairPrecision * gridRows**gridCodeLen
6256
finalLatPrecision = pairPrecision * gridLatFullValue
6357
// Longitude precision of a full length code. pairPrecision * gridCols**gridCodeLen
@@ -170,13 +164,13 @@ func CheckFull(code string) error {
170164
} else if err != ErrNotShort {
171165
return err
172166
}
173-
if firstLat := strings.IndexByte(Alphabet, upper(code[0])) * encBase; firstLat >= latMax*2 {
167+
if firstLat := strings.IndexByte(Alphabet, upper(code[0])) * int(encBase); firstLat >= latMax*2 {
174168
return errors.New("latitude outside range")
175169
}
176170
if len(code) == 1 {
177171
return nil
178172
}
179-
if firstLong := strings.IndexByte(Alphabet, upper(code[1])) * encBase; firstLong >= lngMax*2 {
173+
if firstLong := strings.IndexByte(Alphabet, upper(code[1])) * int(encBase); firstLong >= lngMax*2 {
180174
return errors.New("longitude outside range")
181175
}
182176
return nil
@@ -234,3 +228,29 @@ func clipLatitude(lat float64) float64 {
234228
func normalizeLng(value float64) float64 {
235229
return normalize(value, lngMax)
236230
}
231+
232+
// latitudeAsInteger converts a latitude in degrees into the integer representation.
233+
// It will be clipped into the degree range -90<=x<90 (actually 0-180*2.5e7-1).
234+
func latitudeAsInteger(latDegrees float64) int64 {
235+
latVal := int64(math.Round(latDegrees * finalLatPrecision))
236+
latVal += latMax * finalLatPrecision
237+
if latVal < 0 {
238+
latVal = 0
239+
} else if latVal >= 2*latMax*finalLatPrecision {
240+
latVal = 2*latMax*finalLatPrecision - 1
241+
}
242+
return latVal
243+
}
244+
245+
// longitudeAsInteger converts a longitude in degrees into the integer representation.
246+
// It will be normalised into the degree range -180<=x<180 (actually 0-360*8.192e6).
247+
func longitudeAsInteger(lngDegrees float64) int64 {
248+
lngVal := int64(math.Round(lngDegrees * finalLngPrecision))
249+
lngVal += lngMax * finalLngPrecision
250+
if lngVal <= 0 {
251+
lngVal = lngVal%(2*lngMax*finalLngPrecision) + 2*lngMax*finalLngPrecision
252+
} else if lngVal >= 2*lngMax*finalLngPrecision {
253+
lngVal = lngVal % (2 * lngMax * finalLngPrecision)
254+
}
255+
return lngVal
256+
}

0 commit comments

Comments
 (0)