diff --git a/snowflake.go b/snowflake.go index 194d50f..4d79877 100644 --- a/snowflake.go +++ b/snowflake.go @@ -37,7 +37,7 @@ const encodeBase32Map = "ybndrfg8ejkmcpqxot1uwisza345h769" var decodeBase32Map [256]byte -const encodeBase58Map = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ" +const encodeBase58Map = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" var decodeBase58Map [256]byte @@ -131,6 +131,47 @@ func NewNode(node int64) (*Node, error) { return &n, nil } +func NewNodeWithStartTime(node int64, startTime time.Time) (*Node, error) { + if NodeBits+StepBits > 22 { + return nil, errors.New("Remember, you have a total 22 bits to share between Node/Step") + } + + startUnixMilli := startTime.UnixMilli() + if startUnixMilli > time.Now().UnixMilli() { + return nil, errors.New("Start time cannot be greater than time.Now()") + + } + startEpoch := startTime.UnixMilli() + Epoch = startEpoch + // re-calc in case custom NodeBits or StepBits were set + // DEPRECATED: the below block will be removed in a future release. + mu.Lock() + nodeMax = -1 ^ (-1 << NodeBits) + nodeMask = nodeMax << StepBits + stepMask = -1 ^ (-1 << StepBits) + timeShift = NodeBits + StepBits + nodeShift = StepBits + mu.Unlock() + + n := Node{} + n.node = node + n.nodeMax = -1 ^ (-1 << NodeBits) + n.nodeMask = n.nodeMax << StepBits + n.stepMask = -1 ^ (-1 << StepBits) + n.timeShift = NodeBits + StepBits + n.nodeShift = StepBits + + if n.node < 0 || n.node > n.nodeMax { + return nil, errors.New("Node number must be between 0 and " + strconv.FormatInt(n.nodeMax, 10)) + } + + var curTime = time.Now() + // add time.Duration to curTime to make sure we use the monotonic clock if available + n.epoch = curTime.Add(time.Unix(startEpoch/1000, (startEpoch%1000)*1000000).Sub(curTime)) + + return &n, nil +} + // Generate creates and returns a unique snowflake ID // To help guarantee uniqueness // - Make sure your system is keeping accurate system time diff --git a/snowflake_test.go b/snowflake_test.go index ff750c4..cc76a90 100644 --- a/snowflake_test.go +++ b/snowflake_test.go @@ -4,6 +4,7 @@ import ( "bytes" "reflect" "testing" + "time" ) //****************************************************************************** @@ -23,6 +24,23 @@ func TestNewNode(t *testing.T) { } +func TestNewNodeWithStartTime(t *testing.T) { + _, err := NewNodeWithStartTime(0, time.Now().Add(-1*time.Hour)) // valid node and valid past time + if err != nil { + t.Fatalf("error creating NewNode, %s", err) + } + + _, err = NewNodeWithStartTime(1025, time.Now().Add(-1*time.Hour)) // invalid node but valid past time + if err == nil { + t.Fatalf("no error creating NewNode, %s", err) + } + + _, err = NewNodeWithStartTime(7, time.Now().Add(10*time.Hour)) // valid node but invalid future time + if err == nil { + t.Fatalf("no error creating NewNode, %s", err) + } +} + // lazy check if Generate will create duplicate IDs // would be good to later enhance this with more smarts func TestGenerateDuplicateID(t *testing.T) { @@ -320,6 +338,32 @@ func TestIntBytes(t *testing.T) { } +func TestID_Time(t *testing.T) { + node, err := NewNodeWithStartTime(1000, time.Now().Add(-1*24*2000*time.Hour)) + if err != nil { + t.Fatalf("error creating NewNode, %s", err) + } + + timeForID, err := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) + int64Id := node.Generate().Int64() + snowFlakeID := ID(int64Id) + if timeForID.UnixMilli() > snowFlakeID.Time() { // timeForId is generated first so should be less than int64Id + t.Fatalf("error matching time, %s", err) + } + + node, err = NewNodeWithStartTime(1000, time.Now().Add(-1*time.Hour)) + if err != nil { + t.Fatalf("error creating NewNode, %s", err) + } + + int64Id = node.Generate().Int64() + snowFlakeID = ID(int64Id) + if int64Id < snowFlakeID.Time() { + t.Fatalf("error matching time, %s", err) + } + +} + //****************************************************************************** // Marshall Test Methods