Skip to content

Commit

Permalink
Add base62 versions, plus prefixed versions
Browse files Browse the repository at this point in the history
  • Loading branch information
chilts committed Feb 15, 2025
1 parent 3d2f84b commit bcd0de8
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 18 deletions.
92 changes: 82 additions & 10 deletions ReadMe.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,75 @@
# yid #

These IDs are distributed (needs no co-ordination or central server) and are
timestamped so they are approximately sortable.

The random part however is not monotonically incrementing. There are use-cases
for and against this. For example, you generally want incrementing IDs within
any particular operation(s) which this package will provide. It wouldn't
provide this across parrallel operations, though it would provide unique IDs.

This is just a design decision for this package. If you
need monotonically incrementing IDs you might have to look elsewhere.

## Synopsis ##

```js
const yid = require('yid')

// Standard Version
console.log(yid())
// -> 1517049989798-7496988299172
// -> '1517049989798-7496988299172'

// Base62 Version (Shorter)
console.log(yid.base62())
// -> '0UcpUKiY-0i0Jk7UU'

// Prefixed Standard Version
console.log(yid('cc'))
// -> 'cc-1517056278382-3829387742987'

// Base62 Version (Shorter)
console.log(yid.base62('cc'))
// -> 'cc-0UcpULuS-0dw0bPNj'
```

A `yid` is:
A standard `yid` is:

* always 27 chars long
* has two parts:
* a timestamp
* a random string
* is of the form `\d{13}-\d{13}`
* starts off with `Date.now()`
* uses a substring of https://www.npmjs.com/package/math-random for the second part
* ends with a random set of numbers

A base62 `yid` is:

* always 17 chars long
* has two parts:
* a timestamp
* a random string
* is of the form `[0-9a-zA-Z]{13}-[0-9a-zA-Z]{8}`
* starts off with `Date.now()` in base62
* ends with a random set of numbers in base62

## `yid.fromDate(d)` ##
### `yid.fromDate(d)` ###

Pass in a `Date` object, and get a yid back. This is great if you want to
timestamp something in the past or the future, rather than right now.

You may pass a prefix as the 2nd parameter.


### `yid.base62()` ###

Returns an ID not as `\d{13}-\d{13}` but as
`[0-9A-Za-z]{8}-[0-9A-Za-z]{8}`. i.e. a smaller but still sortable version.

```js
const yid = require('yid')

// get the date at the start of the day
// create an ID using the date at the start of the day
const date = new Date()
date.setUTCHours(0)
date.setUTCMinutes(0)
Expand All @@ -38,7 +79,7 @@ console.log(yid.fromDate(date))
// -> 1635984000000-4266433825250
```

## `yid.asDate(id)` ##
### `yid.asDate(id)` ###

Returns the numbers from the first part of the `id` as a `Date()`.

Expand All @@ -47,7 +88,7 @@ yid.asDate(id)
// -> Date: "2018-01-27T10:46:29.798Z"
```

## `yid.asEpoch(id)` ##
### `yid.asEpoch(id)` ###

Returns the numbers from the first part of the `id` as a `Number()`.

Expand All @@ -69,20 +110,51 @@ yid.asRandom(id)

Why another ID generating library?

I already wrote [zid](https://www.npmjs.com/package/zid) and [flake](https://www.npmjs.com/package/flake) (a long time
ago) and they all have uses. The use for this one is to generate FAST but UNIQUE distributed IDs with no central server
to talk to and no co-ordination required.
I already wrote [zid](https://www.npmjs.com/package/zid) and
[flake](https://www.npmjs.com/package/flake) (a long time ago) and they all
have uses. The use for this one is to generate FAST but UNIQUE distributed IDs
with no central server to talk to and no co-ordination required.

A secondary property is that they are approximately sortable across servers.

I got the idea from Google Keep, since the notes have IDs as follows, whereby the first secion is just `Date.now()`:

* '1517050593526.6629835825', e.g. https://keep.google.com/u/1/#NOTE/1517050593526.6629835825

Similar to Firebase Push IDs and are also sortable via ASCII. However, I also
wanted to avoid the possibility of an initial '-' or '_'. Hence Base62 (not Base64).

## Author ##

Andrew Chilton.

```
╒════════════════════════════════════════════════════╕
│ │
│ Andrew Chilton (Personal) │
│ ------------------------- │
│ │
│ Email : [email protected]
│ Web : https://chilts.org │
│ Twitter : https://twitter.com/andychilton │
│ GitHub : https://github.com/chilts │
│ │
│ Apps Attic Ltd (My Company) │
│ --------------------------- │
│ │
│ Email : [email protected]
│ Web : https://appsattic.com │
│ Twitter : https://twitter.com/AppsAttic │
│ │
│ Node.js / npm │
│ ------------- │
│ │
│ Profile : https://www.npmjs.com/~chilts │
│ Card : $ npx chilts │
│ │
╘════════════════════════════════════════════════════╛
```

## License ##

ISC.
72 changes: 64 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
// npm
const mathRandom = require('math-random')

function pad(id) {
while ( id.length < 27 ) {
function pad(id, len) {
while ( id.length < len ) {
id += '0'
}
return id
}

function yid() {
let id = Date.now() + '-' + String(mathRandom()).substr(2, 13)
return pad(id)
function yid(prefix) {
let id = (prefix ? `${prefix}-` : '') + Date.now() + '-' + pad(String(mathRandom()).substr(2, 13), 13)
return id
}

function fromDate(d) {
let id = d.valueOf() + '-' + String(mathRandom()).substr(2, 13)
return pad(id)
function fromDate(d, prefix) {
let id = (prefix ? `${prefix}-` : '') + d.valueOf() + '-' + pad(String(mathRandom()).substr(2, 13), 13)
return id
}

function asDate(id) {
Expand All @@ -40,9 +41,64 @@ function asRandom(id) {
return id.split('-')[1]
}

// Base62 character set (URL-safe), and also sortable
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

// toBase62()
//
// * toBase62(0) -> '0'
// * toBase62(1) -> '1'
// * toBase62(Date.now()) -> 'UcpTvDM'
// * toBase62(9999999999999) -> '2q3Rktod'
//
// So '2q3Rktod' is the highest random part you'll see.
function toBase62(number) {
if (number === 0) {
return BASE62[0]
}

let result = ''
while (number > 0) {
result = BASE62[number % 62] + result
number = Math.floor(number / 62)
}
return result
}

function fromBase62(str) {
let result = 0

for (let i = 0; i < str.length; i++) {
const char = str[i]
const value = BASE62.indexOf(char)
if (value === -1) {
throw new Error(`Invalid character: ${char}`)
}
result = result * 62 + value
}

return result
}

// always pads at the start of the string
function padStart(str, len) {
return str.padStart(len, '0')
}

function base62(prefix) {
const ts = padStart(toBase62(Date.now()), 8)
const randNum = Number(String(mathRandom()).substr(2, 13))
const randStr = padStart(toBase62(randNum), 8)
return (prefix ? `${prefix}-` : '' ) + ts + '-' + randStr
}

// exports

yid.fromDate = fromDate
yid.asDate = asDate
yid.asEpoch = asEpoch
yid.asRandom = asRandom

yid.base62 = base62

module.exports = yid
44 changes: 44 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const count = {
'38' : 0,
}

let base62Count = 0

// tests
let id = ''

Expand Down Expand Up @@ -49,12 +51,22 @@ for(let i = 0; i < times; i++) {
if (yid.asRandom(id) != id.split('-')[1]) {
throw new Error("the date's random part didn't match the that of the id")
}

// test the base62 version
id = yid.base62()
if ( id.length === 17 ) {
base62Count += 1
}
}

if ( count['27'] !== times ) {
throw new Error("A yid was returned that wasn't 27 chars long")
}

if ( base62Count !== times ) {
throw new Error("A yid.base62() was returned that wasn't 17 chars long")
}

for(let i = 1; i < times; i++ ) {
id = yid()

Expand All @@ -67,3 +79,35 @@ for(let i = 1; i < times; i++ ) {
throw new Error("A yid was returned that wasn't of the format DDDDDDDDDDDDD-DDDDDDDDDDDDD : " + id)
}
}

// check for prefixes (regular)
for(let i = 1; i < times; i++ ) {
id = yid('prj')

if ( id.split('-').length !== 3 ) {
throw new Error("A prefixed yid was returned that didn't have three sections around the dash : " + id)
}

// prj-1517049989798-7496988299172
// console.log('id:', id)
// console.log('match:', id.match(/^prj-\d{13}-\d{13}$/))
if ( !id.match(/^prj-\d{13}-\d{13}$/) ) {
throw new Error("A prefixed yid was returned that wasn't of the format xxx-DDDDDDDDDDDDD-DDDDDDDDDDDDD : " + id)
}
}

// check for prefixes (base62)
for(let i = 1; i < times; i++ ) {
id = yid.base62('prj')

if ( id.split('-').length !== 3 ) {
throw new Error("A prefixed base62 yid was returned that didn't have three sections around the dash : " + id)
}

// prj-0UcpUbzy-2iGH8g3D
// console.log('id:', id)
// console.log('match:', id.match(/^prj-[0-1a-zA-Z]{8}-[0-9a-zA-Z]{8}$/))
if ( !id.match(/^prj-[0-9a-zA-Z]{8}-[0-9a-zA-Z]{8}$/) ) {
throw new Error("A prefixed base62 yid was returned that wasn't of the format xxx-XXXXXXXX-XXXXXXXX : " + id)
}
}

0 comments on commit bcd0de8

Please sign in to comment.