Skip to content

Commit b1247cc

Browse files
committed
Add Go implementation
1 parent 8c4da18 commit b1247cc

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ Implementations
4343
- `javascript
4444
time-hash <https://github.com/disarticulate/time-hash>`__
4545
- port of reference implements
46+
- `golang
47+
timehash <https://github.com/abeusher/timehash/blob/master/timehash.go>`__
48+
- a reference implementation in Go
4649

4750
Usage
4851
-----

timehash.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
This module creates a fuzzy precision representation of a time interval.
3+
- It makes calculations based on a 64 year representation of time from January 1, 1970 to January 1, 2098.
4+
- Values are encoded with a number of bits(represented by an ASCII character) that indicate the amount of time to add to 1970.
5+
- Times prior to 1970 or after 2098 are not accounted for by this scale.
6+
- Each character added to the timehash reduces the time interval ambiguity by a factor of 8.
7+
- Valid characters for encoding the floating point time into ASCII characters include {01abcdef}
8+
9+
0 +/- 64 years
10+
1 +/- 8 years
11+
2 +/- 1 years
12+
3 +/- 45.65625 days
13+
4 +/- 5.707 days
14+
5 +/- 0.71337 days = 17.121 hours
15+
6 +/- 2.14013671875 hours
16+
7 +/- 0.26751708984375 hours = 16.05 minutes
17+
8 +/- 2.006378173828125 minutes
18+
9 +/- 0.2507 minutes = 15 seconds
19+
10 +/- 1.88097 seconds
20+
*/
21+
22+
package main
23+
24+
import (
25+
"fmt"
26+
"strconv"
27+
"strings"
28+
"time"
29+
)
30+
31+
const _base32 string = "01abcdef"
32+
const mapBefore string = "f01abcde"
33+
const mapAfter string = "1abcdef0"
34+
const lowerBound float64 = 0.0
35+
const upperBound float64 = 4039372800.0
36+
const defaultPrecision int = 10
37+
38+
var bits = [3]int{4, 2, 1}
39+
var decodeMap = make(map[string]int)
40+
var neighborMap = make(map[string]Neighbor)
41+
42+
// Neighbor holds the timehash before, during, and after
43+
// the given window to exclude the given timehash.
44+
type Neighbor struct {
45+
before string
46+
after string
47+
}
48+
49+
// Neighborhood holds the timehash before, during, and after
50+
// the given window to include the given timehash.
51+
type Neighborhood struct {
52+
before string
53+
center string
54+
after string
55+
}
56+
57+
func main() {
58+
initMaps()
59+
examples()
60+
}
61+
62+
func examples() {
63+
testHash := "af1cef0" // '2016-05-27T01:55:57.202148'
64+
th, slop := DecodeExactly(testHash)
65+
ths := strconv.FormatFloat(th, 'f', 7, 64)
66+
slops := strconv.FormatFloat(slop, 'f', 7, 64)
67+
fmt.Print("Encoded with margin ", ths, slops, "\n")
68+
fmt.Print("Encoded ", Encode(1516933969.398167, defaultPrecision), "\n")
69+
fmt.Print("Before ", Before(testHash), "\n")
70+
fmt.Print("After: ", After(testHash), "\n")
71+
fmt.Print("Encode from datetime ", EncodeDate(time.Now()), "\n")
72+
}
73+
74+
func initMaps() {
75+
for i := range _base32 {
76+
decodeMap[string(_base32[i])] = i
77+
neighborMap[string(_base32[i])] = Neighbor{string(mapBefore[i]), string(mapAfter[i])}
78+
}
79+
fmt.Println(decodeMap)
80+
}
81+
82+
// DecodeExactly decodes a timehash and get the margin of error
83+
// on that calculation in seconds.
84+
func DecodeExactly(timehash string) (float64, float64) {
85+
lower, upper := lowerBound, upperBound
86+
var mid float64
87+
timeError := (lower + upper) / 2
88+
89+
for _, c := range timehash {
90+
dc := decodeMap[string(c)]
91+
for _, mask := range bits {
92+
timeError = timeError / 2
93+
mid = (lower + upper) / 2
94+
95+
if mask&dc > 0 {
96+
lower = mid
97+
} else {
98+
upper = mid
99+
}
100+
}
101+
}
102+
103+
value := (lower + upper) / 2
104+
return value, timeError
105+
}
106+
107+
// Decode the geohash but throw away the margin of error.
108+
func Decode(timehash string) float64 {
109+
epochSeconds, _ := DecodeExactly(timehash)
110+
return epochSeconds
111+
}
112+
113+
// Encode the given seconds since the epoch into a timehash
114+
// at the given precision.
115+
func Encode(epochSeconds float64, precision int) string {
116+
// If no precision offered (zero value), default to 10.
117+
if precision == 0 {
118+
precision = defaultPrecision
119+
}
120+
var timehash []string
121+
var ch, bit int
122+
lower, upper := lowerBound, upperBound
123+
124+
for len(timehash) < precision {
125+
mid := (lower + upper) / 2
126+
if epochSeconds > mid {
127+
ch = ch | bits[bit]
128+
lower = mid
129+
} else {
130+
upper = mid
131+
}
132+
if bit < 2 {
133+
bit = bit + 1
134+
} else {
135+
timehash = append(timehash, string(_base32[ch]))
136+
bit = 0
137+
ch = 0
138+
}
139+
}
140+
141+
return strings.Join(timehash, "")
142+
}
143+
144+
// Before determines the timehash for the window directly
145+
// preceding the given timehash at the same precision.
146+
func Before(timehash string) string {
147+
i := 1
148+
var value string
149+
150+
for j := len(timehash) - 1; j >= 0; j-- {
151+
c := string(timehash[j])
152+
if c != "0" {
153+
padding := strings.Repeat("f", i-1)
154+
position := len(timehash) - i
155+
value = timehash[0:position] + neighborMap[c].before + padding
156+
break
157+
} else {
158+
i = i + 1
159+
}
160+
}
161+
162+
return value
163+
}
164+
165+
// After determines the timehash for the window directly
166+
// after the given timehash at the same precision.
167+
func After(timehash string) string {
168+
i := 1
169+
var value string
170+
171+
for j := len(timehash) - 1; j >= 0; j-- {
172+
c := string(timehash[j])
173+
if c != "f" {
174+
padding := strings.Repeat("0", i-1)
175+
position := len(timehash) - i
176+
value = timehash[0:position] + neighborMap[c].after + padding
177+
break
178+
} else {
179+
i = i + 1
180+
}
181+
}
182+
183+
return value
184+
}
185+
186+
// Neighbors returns the neighboring windows to either side
187+
// of the provided timehash at the same precision.
188+
func Neighbors(timehash string) Neighbor {
189+
return Neighbor{Before(timehash), After(timehash)}
190+
}
191+
192+
// Expand the given timehash window to include the preceding
193+
// and following window, including the given timehash.
194+
func Expand(timehash string) Neighborhood {
195+
return Neighborhood{Before(timehash), timehash, After(timehash)}
196+
}
197+
198+
// EncodeDate takes a Time object and turns it into a
199+
// timehash at the default precision.
200+
func EncodeDate(date time.Time) string {
201+
return Encode(float64(date.Unix()), defaultPrecision)
202+
}

0 commit comments

Comments
 (0)