Skip to content

Commit 23908c7

Browse files
committed
Add String Representation
1 parent 35e3a06 commit 23908c7

File tree

5 files changed

+287
-2
lines changed

5 files changed

+287
-2
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ console.log(uid);
109109

110110
Randflake ID is designed for high-performance scenarios, with minimal overhead in ID generation.
111111

112+
## String Representation
113+
114+
Randflake ID is encoded as a base32hex string.
115+
116+
```python
117+
base32hexchars = "0123456789abcdefghijklmnopqrstuv"
118+
119+
original = 4594531474933654033
120+
encoded = "3vgoe12ccb8gh"
121+
122+
def decode(s):
123+
return int(s, 32)
124+
125+
def encode(n):
126+
if n < 0:
127+
n += 1 << 64
128+
129+
if n == 0:
130+
return "0"
131+
132+
result = ""
133+
while n > 0:
134+
result = base32hexchars[n&0x1f] + result
135+
n = n // 32
136+
return result
137+
138+
assert original == decode(encode(original))
139+
assert encode(original) == "3vgoe12ccb8gh"
140+
```
141+
112142
## Contributing
113143

114144
Contributions are welcome! Please follow these steps:

randflake.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var (
3535
ErrInvalidNode = errors.New("randflake: invalid node id, node id must be between 0 and 131071")
3636
ErrResourceExhausted = errors.New("randflake: resource exhausted (generator can't handle current throughput, try using multiple randflake instances)")
3737
ErrConsistencyViolation = errors.New("randflake: timestamp consistency violation, the current time is less than the last time")
38+
ErrInvalidID = errors.New("randflake: invalid id")
3839
)
3940

4041
type Generator struct {
@@ -176,6 +177,15 @@ func (g *Generator) Generate() (int64, error) {
176177
return int64(binary.LittleEndian.Uint64(b[:])), nil
177178
}
178179

180+
// GenerateString generates a unique, encrypted ID and returns it as a string.
181+
func (g *Generator) GenerateString() (string, error) {
182+
id, err := g.Generate()
183+
if err != nil {
184+
return "", err
185+
}
186+
return base32hexencode(uint64(id)), nil
187+
}
188+
179189
// Inspect returns the timestamp, node ID, and sequence number of the given ID.
180190
func (g *Generator) Inspect(id int64) (timestamp int64, nodeID int64, sequence int64, err error) {
181191
var b [8]byte
@@ -190,3 +200,63 @@ func (g *Generator) Inspect(id int64) (timestamp int64, nodeID int64, sequence i
190200
sequence = id & RANDFLAKE_MAX_SEQUENCE
191201
return
192202
}
203+
204+
// InspectString returns the timestamp, node ID, and sequence number of the given ID.
205+
func (g *Generator) InspectString(id string) (timestamp int64, nodeID int64, sequence int64, err error) {
206+
num, err := base32hexdecode(id)
207+
if err != nil {
208+
return 0, 0, 0, err
209+
}
210+
return g.Inspect(int64(num))
211+
}
212+
213+
const b32hexchars = "0123456789abcdefghijklmnopqrstuv"
214+
215+
func base32hexencode(num uint64) string {
216+
if num == 0 {
217+
return "0"
218+
}
219+
220+
var encoded [13]byte
221+
idx := 12
222+
for num > 0 {
223+
encoded[idx] = b32hexchars[num&0x1f]
224+
num >>= 5
225+
idx--
226+
}
227+
228+
return string(encoded[idx+1:])
229+
}
230+
231+
func base32hexdecode(s string) (uint64, error) {
232+
var num uint64
233+
for _, c := range s {
234+
if c == '=' {
235+
break
236+
}
237+
238+
num <<= 5
239+
if c >= '0' && c <= '9' {
240+
num += uint64(c - '0')
241+
} else if c >= 'a' && c <= 'v' {
242+
num += uint64(c - 'a' + 10)
243+
} else if c >= 'A' && c <= 'V' {
244+
num += uint64(c - 'A' + 10)
245+
} else {
246+
return 0, ErrInvalidID
247+
}
248+
}
249+
return num, nil
250+
}
251+
252+
func EncodeString(id int64) string {
253+
return base32hexencode(uint64(id))
254+
}
255+
256+
func DecodeString(s string) (int64, error) {
257+
id, err := base32hexdecode(s)
258+
if err != nil {
259+
return 0, err
260+
}
261+
return int64(id), nil
262+
}

randflake_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package randflake
33
import (
44
"crypto/rand"
55
"encoding/binary"
6+
"encoding/hex"
7+
"math"
8+
"strconv"
69
"sync/atomic"
710
"testing"
811
"time"
@@ -279,3 +282,145 @@ func TestGenerator_Inspect(t *testing.T) {
279282
t.Errorf("Expected counter %d, got %d", counter, counter2)
280283
}
281284
}
285+
286+
func TestBase32HexEncode(t *testing.T) {
287+
tests := []uint64{
288+
0,
289+
1,
290+
10,
291+
100,
292+
1000,
293+
10000,
294+
100000,
295+
1000000,
296+
10000000,
297+
100000000,
298+
1000000000,
299+
10000000000,
300+
100000000000,
301+
1000000000000,
302+
10000000000000,
303+
100000000000000,
304+
1000000000000000,
305+
10000000000000000,
306+
100000000000000000,
307+
1000000000000000000,
308+
math.MaxUint64,
309+
math.MaxInt64,
310+
}
311+
312+
for i := range tests {
313+
enc1 := strconv.FormatUint(tests[i], 32)
314+
enc2 := base32hexencode(tests[i])
315+
if enc1 != enc2 {
316+
t.Errorf("Expected %s, got %s", enc1, enc2)
317+
t.Fail()
318+
return
319+
}
320+
}
321+
}
322+
323+
func TestBase32HexDecode(t *testing.T) {
324+
tests := []uint64{
325+
0,
326+
1,
327+
10,
328+
100,
329+
1000,
330+
10000,
331+
100000,
332+
1000000,
333+
10000000,
334+
100000000,
335+
1000000000,
336+
10000000000,
337+
100000000000,
338+
1000000000000,
339+
10000000000000,
340+
100000000000000,
341+
1000000000000000,
342+
10000000000000000,
343+
100000000000000000,
344+
1000000000000000000,
345+
math.MaxUint64,
346+
math.MaxInt64,
347+
}
348+
349+
for i := range tests {
350+
dec1, err := strconv.ParseUint(base32hexencode(tests[i]), 32, 64)
351+
if err != nil {
352+
t.Errorf("Error decoding %d: %v", tests[i], err)
353+
t.Fail()
354+
return
355+
}
356+
dec2, err := base32hexdecode(base32hexencode(tests[i]))
357+
if err != nil {
358+
t.Errorf("Error decoding %d: %v", tests[i], err)
359+
t.Fail()
360+
return
361+
}
362+
if dec1 != dec2 {
363+
t.Errorf("Expected %d, got %d", dec1, dec2)
364+
t.Fail()
365+
return
366+
}
367+
}
368+
}
369+
370+
func TestInspectString(t *testing.T) {
371+
keyString := "dffd6021bb2bd5b0af676290809ec3a5"
372+
encoded := "3vgoe12ccb8gh"
373+
374+
key, err := hex.DecodeString(keyString)
375+
if err != nil {
376+
t.Fatalf("Failed to decode key: %v", err)
377+
}
378+
379+
id, err := DecodeString(encoded)
380+
if err != nil {
381+
t.Fatalf("Failed to decode ID: %v", err)
382+
}
383+
384+
if id != 4594531474933654033 {
385+
t.Errorf("Expected ID 4594531474933654033, got %d", id)
386+
}
387+
388+
g, err := NewGenerator(1, time.Now().Unix(), time.Now().Unix()+3600, key)
389+
if err != nil {
390+
t.Fatalf("Failed to create generator: %v", err)
391+
}
392+
393+
timestamp0, nodeID0, counter0, err := g.Inspect(int64(id))
394+
if err != nil {
395+
t.Fatalf("Failed to inspect ID: %v", err)
396+
}
397+
398+
if timestamp0 != 1733706297 {
399+
t.Errorf("Expected timestamp 1733706297, got %d", timestamp0)
400+
}
401+
402+
if nodeID0 != 42 {
403+
t.Errorf("Expected node ID 42, got %d", nodeID0)
404+
}
405+
406+
if counter0 != 1 {
407+
t.Errorf("Expected counter 1, got %d", counter0)
408+
}
409+
410+
timestamp1, nodeID1, counter1, err := g.InspectString(encoded)
411+
if err != nil {
412+
t.Fatalf("Failed to inspect ID: %v", err)
413+
}
414+
415+
if timestamp1 != 1733706297 {
416+
t.Errorf("Expected timestamp 1733706297, got %d", timestamp1)
417+
}
418+
419+
if nodeID1 != 42 {
420+
t.Errorf("Expected node ID 42, got %d", nodeID1)
421+
}
422+
423+
if counter1 != 1 {
424+
t.Errorf("Expected counter 1, got %d", counter1)
425+
}
426+
}

src/randflake/randflake.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ def __init__(self):
6262
)
6363

6464

65+
class ErrInvalidID(RandflakeError):
66+
def __init__(self):
67+
super().__init__("randflake: invalid id")
68+
69+
70+
_base32hexchars = "0123456789abcdefghijklmnopqrstuv"
71+
72+
73+
def _encodeB32hex(n):
74+
if n < 0:
75+
n += 1 << 64
76+
77+
if n == 0:
78+
return "0"
79+
80+
result = ""
81+
while n > 0:
82+
result = base32hexchars[n & 0x1F] + result
83+
n = n // 32
84+
return result
85+
86+
87+
def _decodeB32hex(s):
88+
return int(s, 32)
89+
90+
6591
class Generator:
6692
def __init__(self, node_id: int, lease_start: int, lease_end: int, secret: bytes):
6793
if lease_end < lease_start:
@@ -134,6 +160,10 @@ def generate(self) -> int:
134160
self.sbox.encrypt(dst, src)
135161
return struct.unpack("<q", dst)[0]
136162

163+
def generate_string(self) -> str:
164+
_id = self.generate()
165+
return _encodeB32hex(_id)
166+
137167
def inspect(self, id_val: int) -> Tuple[int, int, int]:
138168
src = struct.pack("<q", id_val)
139169
dst = bytearray(8) # Use bytearray for dst
@@ -150,3 +180,7 @@ def inspect(self, id_val: int) -> Tuple[int, int, int]:
150180
sequence = id_raw & RANDFLAKE_MAX_SEQUENCE
151181

152182
return timestamp, node_id, sequence
183+
184+
def inspect_string(self, id_str: str) -> Tuple[int, int, int]:
185+
id_val = _decodeB32hex(id_str)
186+
return self.inspect(id_val)

src/randflake/test_randflake.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,14 @@ def test_inspect(self):
202202
secret_bytes = bytes.fromhex(secret)
203203
g = Generator(1, time.time(), time.time() + 3600, secret_bytes)
204204

205-
id = 4594531474933654033
206-
timestamp, node_id, counter = g.inspect(id)
205+
_id = 4594531474933654033
206+
timestamp, node_id, counter = g.inspect(_id)
207+
self.assertEqual(timestamp, 1733706297)
208+
self.assertEqual(node_id, 42)
209+
self.assertEqual(counter, 1)
210+
211+
_id_str = "3vgoe12ccb8gh"
212+
timestamp, node_id, counter = g.inspect_string(_id_str)
207213
self.assertEqual(timestamp, 1733706297)
208214
self.assertEqual(node_id, 42)
209215
self.assertEqual(counter, 1)

0 commit comments

Comments
 (0)