Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ PATH
remote: .
specs:
torid (1.3.0)
fnv (~> 0.2)

GEM
remote: https://rubygems.org/
specs:
builder (3.2.4)
docile (1.4.0)
fnv (0.2.0)
jar-dependencies (0.4.1)
minitest (5.22.2)
minitest-focus (1.4.0)
Expand Down
6 changes: 6 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# Torid Changelog
## Version 2.0.0 - 2024-03-XX

* Refactor entire library to be a UUIDv6, UUIDv7, UUIDv8 implementation.
* Remove dependency on very old fnv gem and reimplment fnv algorithm internally
* Implement the crockford base 32 algorithm for alternative display format

## Version 1.3.0 - 2017-02-17

* Add Torid::UUID#node_id_s to allow access to just the node portion
Expand Down
111 changes: 102 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Torid
# Torid

[![Build Status](https://copiousfreetime.semaphoreci.com/badges/torid/branches/main.svg)](https://copiousfreetime.semaphoreci.com/projects/torid)

Expand All @@ -7,8 +7,8 @@

## DESCRIPTION

Temporally Ordered IDs. Generate universally unique identifiers (UUID)
that sort lexically in time order.
A library to generate UUID's that are temporally ordered when sorted
lexigraphically. Torid implements UUIDv6, UUIDv7, and UUIDv8.

## DETAILS

Expand All @@ -24,10 +24,8 @@ events that are entering a system with the following criteria:
5. Eventually stored in a UUID field in a database. So 128bit ids are totally
fine.

The IDs that Torid generates are 128bit IDs made up of 2, 64bit parts.

* 64bit microsecond level UNIX timestamp
* 64bit hash of the system hostname, process id and a random value.
Torid generates these id's in multiple different algorithms, you may choose
which one you want.

## EXAMPLES

Expand All @@ -36,9 +34,19 @@ The IDs that Torid generates are 128bit IDs made up of 2, 64bit parts.
```ruby
require 'torid'

# Generate a UUIDv7 that defaults to UUID format
uuid = Torid.uuid
uuid.to_s # => "0004fda4-318e-f380-5a45-5321cd065b02"
uuid.bytes # => "\x00\x04\xFD\xA41\x8E\xF3\x80ZES!\xCD\x06[\x02"
uuid.as_ulid # =>
uuid.to_i # =>

# Generator a UUIDv7 that defaults to ULID display format
ulid = Torid.ulid
ulid.to_s # =>
ulid.bytes # => "\x00\x04\xFD\xA41\x8E\xF3\x80ZES!\xCD\x06[\x02"
ulid.as_uuid # => "0004fda4-318e-f380-5a45-5321cd065b02"
ulid.to_i # =>
```

#### Using your own instance of a Generator
Expand All @@ -51,17 +59,103 @@ uuid = generator.next

uuid.to_s # => "0004fda4-3f42-3d01-4731-5a4aa8ddd6c3"
uuid.bytes # => "\x00\x04\xFD\xA4?B=\x01G1ZJ\xA8\xDD\xD6\xC3"
uuid.as_ulid # =>
```

#### Configure the Generator

```ruby

# Default uuidv7 generator
uuidv7_generator = Torid::Generator.new

# Default uuidv7 generator that defaults to ulid string format
ulid_generator = Torid::Generator.new(format: :ulid)

# uuidv8 generator that defaults to ulid string format
ulid_v8_generator = Torid::Generator.new(format: :ulid, algorithm: :v8)

```

## UUID Representation

All the UUID values generated by Torid may be displayed in any of the following
formats:

* the standard 8-4-4-4-12 hexadecimal UUID format
* a base 32 crockford encoded string, also known as [ULID format](https://github.com/ulid/spec)
* an array of 16 bytes
* a 128 bit integer

## Algorithms

Torid implments all the UUID formats detailed in [New UUID Formats
RFC](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format).

The default algorithm for Torid is the UUIDv7 algorithm.

Torid's original custom algorithm is now the UUIDv8 implementation.

### UUIDv6

This is a field-compatible version of UUIDv1, with the time bits reordered so
they sort lexically in time order. You probably don't want this unless you are
migrating from a UUIDv1 system.

The UUIDv6 uses a 60 bit timestamp of the number of 100 nanosecond intervals
(0.1 microseconds) from 00:00:00.00 15 Oct 1585.

* 48 most significant bits of a 60 bit timestamp
* 4 bits containing the UUIDv6 version (0110)
* 12 least significant bits of the 60 bit timestamp
* 2 bits (10) for the UUID variant
* 14 bit clock sequence value
* 48 bit node_id

### UUIDv7

I am also calling this the [ULID implementation](https://github.com/ulid/spec)
and is aliased as such.

* 48 bit timestamp of the number of milliseconds from UNIX Epoch
* 4 bits containing the UUIDv7 version (0111)
* 12 bit of pseudo random data
* 2 bit variant (10)
* 62 bits of pseudo random data

### UUIDv8

Torid had an original temporally ordered uuid generation algorithm that existed
before the new UUIDv7/8/9 standard was proposed. That algorithm is now slightly
altered and is the UUIDv8 implementation.

The IDs that Torid generated in version 1.x were 128bit IDs made up of 2, 64bit
parts.

* 64bit microseconds since the UNIX epoch
* 64bit hash of the system hostname, process id and a random value.

In version 2.x the custom algorithm is the same, but the bit fields are now:

* 48 most significant bits of a 64bit microseconds since the UNIX epoch
* 4 bits containing the UUIDv8 version (1000)
* 12 middle significant bits of the 64bit microseconds since the UNIX epoch
* 2 bit variant (10)
* 4 least significant bits of the 64bit microseconds since the UNIX epoch
* 58 bits of the system hostname, process id and a random value.

## CREDITS / RESOURCES

The vast majority of the credit and research stems from:

* [jondot's](https://github.com/jondot) blog post on [Fast ID Generation](http://blog.paracode.com/2012/04/16/fast-id-generation-part-1/) served to solidify my thoughts on the criteria I needed in an ID generation system.
* This let me to [Boundary's Flake](http://boundary.com/blog/2012/01/12/flake-a-decentralized-k-ordered-unique-id-generator-in-erlang/)
* [James Golick's](https://github.com/jamesgolick) [lexical_uuid](https://github.com/jamesgolick/lexical_uuid), which if I had found a day earlier, I might be using instead of creating this.
* [ulid specification](https://github.com/ulid/spec)
* [draft-peabody-dispatch-new-uuid-format-04](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format)

You could consider Torid to be a reimplementation of [lexical_uuid](https://github.com/jamesgolick/lexical_uuid). It definately steals some code from it and [simple_uuid](https://github.com/cassandra-rb/simple_uuid)
and an implementation of [ulid](https://github.com/ulid/spec).

Blog posts around ID generation:

Expand Down Expand Up @@ -90,8 +184,7 @@ Copyright (c) 2014 Jeremy Hinegardner

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice
and this permission notice appear in all copies.
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
Expand Down
3 changes: 1 addition & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ This.email = "[email protected]"
This.homepage = "http://github.com/copiousfreetime/#{ This.name }"

This.ruby_gemspec do |spec|
spec.add_dependency( 'fnv', '~> 0.2' )

spec.add_development_dependency( 'rake', '~> 13.0')
spec.add_development_dependency( 'minitest', '~> 5.21' )
spec.add_development_dependency( 'minitest-junit', '~> 1.1' )
Expand All @@ -27,3 +25,4 @@ This.ruby_gemspec do |spec|
end

load 'tasks/default.rake'
load 'tasks/custom.rake'
10 changes: 6 additions & 4 deletions lib/torid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# that sort lexically in time order.
module Torid
# Public: The Version of the Torid library as a String
VERSION = "1.3.0"
VERSION = "2.0.0"

# Public: return the next Torid::UUID from the default Generator
#
Expand All @@ -17,6 +17,8 @@ def self.uuid
Torid::Generator.next
end
end
require 'torid/clock'
require 'torid/uuid'
require 'torid/generator'
require_relative 'torid/clock'
require_relative 'torid/crockford'
require_relative 'torid/fnv'
require_relative 'torid/uuid'
require_relative 'torid/generator'
68 changes: 68 additions & 0 deletions lib/torid/crockford.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module Torid
# Internal:
#
# An implementation of Crockford's Base32 encoding and decoding, without the
# check digit.
#
# https://www.crockford.com/base32.html
module Crockford
ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".freeze
DASH = "-".freeze
REGEX = %r{([#{DASH}#{ENCODING}]+)}i

BITS_PER_CODE = 5
BASE_RADIX = (1 << BITS_PER_CODE)

# Precalculate the string to integer mapping for the decoding to account for
# case insensitivity and confusing characters
#
TO_0 = "0Oo".freeze
TO_1 = "1IiLl".freeze

DECODING = Hash.new.tap do |h|
ENCODING.chars.each_with_index do |char, value|
h[char] = value
h[char.downcase] = value
end
TO_0.each_char do |char|
h[char] = 0
end
TO_1.each_char do |char|
h[char] = 1
end
end.freeze

# Internal: Encode an integer into a Crockford Base32 string
#
# Example:
#
# Crockkford.encode( 1234567890 ) # => "14SC0PJ"
#
def self.encode( value )
case value
when Integer
value.digits(BASE_RADIX).map { |i| ENCODING[i] }.reverse.join
else
raise ArgumentError, "#{int} must be an Integer"
end
end

# Internal: Decode a Crockford Base32 string into an integer
#
# Example:
# Crockford.decode( "14SC0PJ" ) # => 1234567890
# Crockford.decode( "14SCoPK" ) # => 1234567890
# Crockford.decode( "14SCOPK" ) # => 1234567890
# Crockford.decode( "l4SCOPK" ) # => 1234567890
# Crockford.decode( "L4SCOPK" ) # => 1234567890
# Crockford.decode( "L4S-COPK" ) # => 1234567890
#
def self.decode( str )
str.chars
.reject { |c| c == DASH }
.reduce(0) do |acc, c|
acc = acc * BASE_RADIX + DECODING[c]
end
end
end
end
Loading