Skip to content

Instantly share code, notes, and snippets.

@amilos
Last active March 10, 2016 19:22
Show Gist options
  • Save amilos/fbdbc14ebc605c2f1435 to your computer and use it in GitHub Desktop.
Save amilos/fbdbc14ebc605c2f1435 to your computer and use it in GitHub Desktop.
Friendly ID generator that creates 10-character string identifiers
using System;
using System.Linq;
using System.Text;
namespace Asseco.MiscUtils
{
/*
* Based on Firebase ids but modified for shorter ids and base62 alphabet
*/
/*
* Friendly ID generator that creates 10-character string identifiers with the following properties:
*
* 1. They contain 30-bits of data from timestamp so that they sort *after* any existing ids.
* 2. They contain 30-bits of random data after the timestamp so that IDs won't collide with other clients' IDs.
* 3. They sort *lexicographically* (so the timestamp is converted to characters that will sort properly).
* 4. They're monotonically increasing. Even if you generate more than one in the same timestamp, the
* latter ones will sort after the former ones. We do this by using the previous random bits
* but "incrementing" them by 1 (only in the case of a timestamp collision).
* 5. With default 5 + 5 characters, in parallel id generation, here are some probabilities of collision
* - 1 in million chance of collision with ~90,000 ids per second
* - 1 in billion chance of collision with ~3,000 ids per second
*/
public class FriendlyId
{
const int lenRandom = 5;
const int lenTimestamp = 5;
const int numChars = lenTimestamp + lenRandom;
// UNIX time epoch
static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// Modeled after base62 web-safe chars, but ordered by ASCII
static string alphabet62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
static long lastPushTime = 0;
static int[] lastRandChars = new int[lenRandom];
private static string Generate()
{
var id = new char[numChars];
var now = (long)((DateTime.UtcNow - epoch).TotalMilliseconds);
bool duplicateTime = now == lastPushTime;
lastPushTime = now;
//Generate first lenTimestamp characters from timestamp, more than 8 does not make sense
for (int i = lenTimestamp - 1; i >= 0; i--)
{
id[i] = alphabet62.ElementAt((int)(now % 62));
now = (long)Math.Floor(now / 62.0);
}
//generate next lenRandom characters from RNG, less than 2 does not make sense
if (!duplicateTime)
{
for (int i = 0; i < lenRandom; i++) //rand
{
lastRandChars[i] = StaticRandom.Next(62);
id[lenTimestamp + i] = alphabet62.ElementAt(lastRandChars[i]);
}
}
else
{
//rand-1
int i = lenRandom - 1;
for (; i >= 0 && lastRandChars[i] == 61; i--)
{
lastRandChars[i] = 0;
}
lastRandChars[i]++;
}
for (int i = 0; i < lenRandom; i++)
{
id[lenTimestamp + i] = alphabet62.ElementAt(lastRandChars[i]);
}
return new string(id);
}
public static string Next { get { return Generate(); } }
public static string NextFormatted
{
get
{
StringBuilder sb = new StringBuilder(Generate(), numChars + 1);
sb.Insert(5, '-');
return sb.ToString();
}
}
// Modified from Yoda Time misc utils
internal static class StaticRandom
{
static Random random = new Random();
static object myLock = new object();
internal static int Next(int max)
{
lock (myLock)
{
return random.Next(max);
}
}
}
}
}
@amilos
Copy link
Author

amilos commented Mar 10, 2016

Usage:

   var id = FriendlyId.Next;
   var formattedId = FriendlyId.NextFormatted;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment