diff --git a/config.go b/config.go index c12ef04..f5d5254 100644 --- a/config.go +++ b/config.go @@ -54,18 +54,18 @@ func NewTinDConfig() *Config { // // This does slow down initialization, but the final product still runs fast to // generate ids -func (c *Config) WithRuneset(r string) *Config { +func (c *Config) WithRuneset(r []rune) *Config { c.runes = ensureUniqueRunes(r) c.runesize = len(c.runes) c.mod = byte(c.runesize - 1) return c } -func ensureUniqueRunes(r string) []rune { +func ensureUniqueRunes(r []rune) []rune { unq := make(map[rune]int) var index int // loop through the runes to find duplicates - for _, v := range []rune(r) { + for _, v := range r { // if the current rune hasn't been seen yet, assign it if _, ok := unq[v]; !ok { unq[v] = index @@ -97,17 +97,21 @@ func (c *Config) Zero() TinD { // Gen generates a TinD with the given configuration func (c *Config) Gen() TinD { - t := TinD{ - bytes: make([]byte, c.size), - config: c, - } - _, err := rand.Read(t.bytes) + bytes := make([]byte, c.size) + runes := make([]rune, c.size) + _, err := rand.Read(bytes) if err != nil { log.Fatalln(err) } // Make sure each byte fits the rune so it can be encoded and decoded for i := 0; i < c.size; i++ { - t.bytes[i] = t.bytes[i] & c.mod + bytes[i] = bytes[i] & c.mod + runes[i] = c.runes[bytes[i]] + } + t := TinD{ + bytes: bytes, + runes: runes, + config: c, } return t } @@ -119,12 +123,15 @@ func (c *Config) FromString(in string) (TinD, error) { if len(in) != c.size { return c.Zero(), errors.New("given id isn't the same size as the configuration") } - t := make([]byte, c.size) + t := TinD{ + bytes: make([]byte, c.size), + config: c, + } r := []rune(in) - for i := 0; i < l; i++ { + for i := 0; i < c.size; i++ { for j := 0; j < c.runesize; j++ { - if r[i] == alphabet[j] { - t[i] = byte(j) + if r[i] == c.runes[j] { + t.bytes[i] = byte(j) break } } diff --git a/tind.go b/tind.go index 012913c..d0fd5be 100644 --- a/tind.go +++ b/tind.go @@ -10,11 +10,6 @@ package tind // TinD is at it's core a byte slice. It's default alphabet can be replaced with // whatever unicode characters you want, as the alphabet it uses is just a slice // of runes -import ( - "crypto/rand" - "errors" - "log" -) // defaults are just a call to NewConfig() var defaults = NewTinDConfig() @@ -24,9 +19,9 @@ var defaults = NewTinDConfig() // Each byte can be the value 0 -> len(alphabet) that is used, to ensure // values can be encoded and decoded reliably. The longer the alphabet // used, the more randomness can be done. -type TinD struct{ - bytes []byte - str string +type TinD struct { + bytes []byte + runes []rune config *Config } @@ -37,11 +32,12 @@ func Gen() TinD { // String returns the Human Readable form of the TinD func (t TinD) String() string { - if len(t.str) == t.config.size { - return t.str - } - t.str = makeString(t.bytes, t.config.size, t.config.runset) - return t.str + return string(t.runes) +} + +// Runes returns the Rune array of the TinD +func (t TinD) Runes() []rune { + return t.runes } // Bytes returns the raw bytes of the TinD @@ -49,10 +45,3 @@ func (t TinD) Bytes() []byte { return t.bytes } -func makeString(bytes []byte, size int, runeset []rune) string { - r := make([]rune, size) - for i := 0; i < size; i++ { - r[i] = runeset[bytes[i]] - } - return string(r) -} diff --git a/tind_test.go b/tind_test.go new file mode 100644 index 0000000..e7dc078 --- /dev/null +++ b/tind_test.go @@ -0,0 +1,78 @@ +package tind + +import ( + "log" + "testing" + "time" +) + +func Test_GenTinD_With_Defaults(t *testing.T) { + id := Gen() + if len(id.String()) != 4 { + t.Errorf("expecting 4 bytes, got %d", len(id.String())) + } +} + +func Test_GenTinD_With_VaryingSizes(t *testing.T) { + tests := []struct { + size int + runes []rune + expectedRuneSize int + }{ + { + size: 4, + runes: []rune("abcdefghijklmnopqrstuvwxyz"), + expectedRuneSize: 26, + }, { + size: 8, + runes: []rune("HGHHGHGJJHSHGABBJEBJBE"), + expectedRuneSize: 7, + }, { + size: 8, + runes: []rune("😀😡🤕🎃🤚🫲🙀😭"), + expectedRuneSize: 8, + }, + } + for _, v := range tests { + tt := NewTinDConfig().WithRuneset(v.runes).WithSize(v.size).Gen() + if len(tt.Runes()) != v.size { + t.Errorf("config set size to %d, but generated an id of size %d. id: %s", v.size, len([]rune(tt.String())), tt.String()) + } + if tt.config.runesize != v.expectedRuneSize { + t.Errorf("expected runsize of %d, got runesize of %d", len(v.runes), tt.config.runesize) + } + } +} + +//func Test_Generate_With_Collisions(t *testing.T) { +// log.Println("Test TinD for collisions, base on number of iterations and time") +// +// maxSize := 16 +// for size := 4; size < maxSize; size++ { +// CheckCollisions(size) +// } +//} + +func CheckCollisions(tindSize int) { + ids := make(map[string]struct{}) + collided := false + tc := NewTinDConfig().WithSize(tindSize) + var iters uint64 + start := time.Now() + + log.Printf("Starting check with size %d", tindSize) + for !collided { + iters++ + nt := tc.Gen() + if _, ok := ids[nt.String()]; ok { + collided = true + } else { + ids[nt.String()] = struct{}{} + } + // Print a message every 2000000 iterations saying we are still working + if iters%2000000 == 0 { + log.Printf("Still no collions on size %d after %d iterations", tindSize, iters) + } + } + log.Printf("Collision found after %v and %d iterations with size of %d", time.Since(start), iters, tindSize) +}