├── logo.png ├── .github ├── FUNDING.yml └── workflows │ └── ruby.yml ├── lib ├── ulid │ ├── version.rb │ └── generator.rb └── ulid.rb ├── spec ├── spec_helper.rb └── lib │ └── ulid_spec.rb ├── Gemfile ├── Rakefile ├── .rubocop.yml ├── CHANGELOG.md ├── ulid.gemspec ├── Gemfile.lock ├── LICENSE ├── .gitignore └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelsales/ulid/HEAD/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [rafaelsales] 4 | -------------------------------------------------------------------------------- /lib/ulid/version.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | module ULID 3 | VERSION = '1.4.0' 4 | end 5 | -------------------------------------------------------------------------------- /lib/ulid.rb: -------------------------------------------------------------------------------- 1 | require 'ulid/version' 2 | require 'ulid/generator' 3 | 4 | module ULID 5 | extend Generator 6 | end 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'byebug' 2 | require 'minitest/autorun' 3 | require 'minitest/pride' 4 | require 'ulid' 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development, :test do 6 | gem 'byebug' 7 | gem 'rake' 8 | gem 'rubocop' 9 | end 10 | 11 | group :test do 12 | gem 'base32-crockford' 13 | gem 'minitest' 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | require 'benchmark' 4 | require 'ulid' 5 | 6 | Rake::TestTask.new do |t| 7 | t.libs += %w[spec lib] 8 | t.test_files = FileList['spec/**/*_spec.rb'] 9 | end 10 | 11 | desc 'Benchmark base32 ULID generation (default 100,000 iterations)' 12 | task :benchmark, [:iterations] do |_t, args| 13 | iterations = (args[:iterations] || 100_000).to_i 14 | Benchmark.bm do |b| 15 | b.report("#{iterations} iterations") { iterations.times { ULID.generate } } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Style/Documentation: 2 | Enabled: false 3 | 4 | Style/TrailingCommaInArguments: 5 | EnforcedStyleForMultiline: comma 6 | 7 | Style/TrailingCommaInArrayLiteral: 8 | EnforcedStyleForMultiline: comma 9 | 10 | Style/TrailingCommaInHashLiteral: 11 | EnforcedStyleForMultiline: comma 12 | 13 | Metrics/AbcSize: 14 | Enabled: false 15 | 16 | BlockLength: 17 | Enabled: false 18 | 19 | Metrics/LineLength: 20 | Max: 120 21 | 22 | Metrics/MethodLength: 23 | Enabled: false 24 | 25 | Style/RescueStandardError: 26 | Enabled: false 27 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | pull_request: ~ 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | bundler-cache: true # Run "bundle install", and cache the result automatically. 24 | - name: Run tests 25 | run: | 26 | bundle exec rake test 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.4.0 2 | 3 | - PR #32 - Allow fully deterministic ULID generation by providing both the timestamp 4 | component and a string suffix to replace the randomness component. Thanks @andjosh 5 | 6 | # 1.2.0 7 | 8 | - PR #20 - Use an array to improve speed / reduce memory allocations. Thanks, @jamescook 9 | 10 | # 1.1.1 11 | 12 | - PR #19 - Remove Timecop gem. Thanks, @bquorning 13 | 14 | - PR #18 - Remove Mocha gem. Thanks, @bquorning 15 | 16 | - PR #17 - Add post_install_message to install sysrandom gem 17 | 18 | - PR #16 - Remove circular require warning. Thanks, @pocke 19 | 20 | # 1.1.0 21 | 22 | - PR #14 - Ruby 2.5+ does not need sysrandom. Thanks, @sakuro 23 | 24 | # 1.0.0 25 | 26 | - PR #10 - Fix time encoding. Thanks, @dcuddeback 27 | -------------------------------------------------------------------------------- /ulid.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require_relative 'lib/ulid/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'ulid' 7 | spec.version = ULID::VERSION 8 | spec.authors = ['Rafael Sales'] 9 | spec.email = ['rafaelcds@gmail.com'] 10 | spec.summary = 'Universally Unique Lexicographically Sortable Identifier implementation for Ruby' 11 | spec.homepage = 'https://github.com/rafaelsales/ulid' 12 | spec.license = 'MIT' 13 | 14 | spec.files = `git ls-files -z`.split("\x0") 15 | spec.files = %w[ulid.gemspec README.md CHANGELOG.md LICENSE] + `git ls-files | grep -E '^(lib)'`.split("\n") 16 | spec.require_paths = ['lib'] 17 | spec.required_ruby_version = '>= 2.6.8' 18 | end 19 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ulid (1.4.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | base32-crockford (0.1.0) 11 | byebug (11.1.3) 12 | json (2.6.3) 13 | minitest (5.18.0) 14 | parallel (1.22.1) 15 | parser (3.2.1.0) 16 | ast (~> 2.4.1) 17 | rainbow (3.1.1) 18 | rake (13.0.6) 19 | regexp_parser (2.7.0) 20 | rexml (3.2.5) 21 | rubocop (1.47.0) 22 | json (~> 2.3) 23 | parallel (~> 1.10) 24 | parser (>= 3.2.0.0) 25 | rainbow (>= 2.2.2, < 4.0) 26 | regexp_parser (>= 1.8, < 3.0) 27 | rexml (>= 3.2.5, < 4.0) 28 | rubocop-ast (>= 1.26.0, < 2.0) 29 | ruby-progressbar (~> 1.7) 30 | unicode-display_width (>= 2.4.0, < 3.0) 31 | rubocop-ast (1.27.0) 32 | parser (>= 3.2.1.0) 33 | ruby-progressbar (1.13.0) 34 | unicode-display_width (2.4.2) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | base32-crockford 41 | byebug 42 | minitest 43 | rake 44 | rubocop 45 | ulid! 46 | 47 | BUNDLED WITH 48 | 2.4.7 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Rafael Sales 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | -------------------------------------------------------------------------------- /lib/ulid/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'securerandom' 4 | 5 | module ULID 6 | module Generator 7 | ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.bytes.freeze # Crockford's Base32 8 | RANDOM_BITS = 80 9 | ENCODED_LENGTH = 26 10 | BITS_PER_B32_CHAR = 5 11 | ZERO = '0'.ord 12 | MASK = 0x1f 13 | 14 | # Generates a 128-bit ULID string. 15 | # @param [Time] time (Time.now) Timestamp - first 48 bits 16 | # @param [String] suffix (80 random bits) - the remaining 80 bits as hex encodable string 17 | def generate(time = Time.now, suffix: nil) 18 | (hi, lo) = generate_bytes(time, suffix: suffix).unpack('Q>Q>') 19 | if hi.nil? || lo.nil? 20 | raise ArgumentError, 'suffix string without hex encoding passed to ULID generator' 21 | end 22 | 23 | integer = (hi << 64) | lo 24 | encode(integer, ENCODED_LENGTH) 25 | end 26 | 27 | # Generates a 128-bit ULID. 28 | # @param [Time] time (Time.now) Timestamp - first 48 bits 29 | # @param [String] suffix (80 random bits) - the remaining 80 bits as hex encodable string 30 | def generate_bytes(time = Time.now, suffix: nil) 31 | suffix_bytes = 32 | if suffix 33 | suffix.split('').map { |char| char.to_i(32) }.pack('C*') 34 | else 35 | SecureRandom.random_bytes(RANDOM_BITS / 8) 36 | end 37 | 38 | time_48bit(time) + suffix_bytes 39 | end 40 | 41 | private 42 | 43 | # Encodes a 128-bit integer input as a 26-character ULID string using Crockford's Base32. 44 | def encode(input, length) 45 | encoded = Array.new(length, ZERO) 46 | i = length - 1 47 | 48 | while input > 0 49 | encoded[i] = ENCODING[input & MASK] 50 | input >>= BITS_PER_B32_CHAR 51 | i -= 1 52 | end 53 | 54 | encoded.pack('c*') 55 | end 56 | 57 | # Returns the first 6 bytes of a timestamp (in milliseconds since the Unix epoch) as a 48-bit byte string 58 | def time_48bit(time = Time.now) 59 | # Avoid `time.to_f` since we want to accurately represent a whole number of milliseconds: 60 | # 61 | # > time = Time.new(2020, 1, 5, 7, 3, Rational(2, 1000)) 62 | # => 2020-01-05 07:03:00 +0000 63 | # > (time.to_f * 1000).to_i 64 | # => 1578207780001 65 | # 66 | # vs 67 | # 68 | # > (time.to_r * 1000).to_i 69 | # => 1578207780002 70 | time_ms = (time.to_r * 1000).to_i 71 | [time_ms].pack('Q>')[2..-1] 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/lib/ulid_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'base32/crockford' 3 | 4 | describe ULID do 5 | describe 'textual representation' do 6 | it 'ensures it has 26 chars' do 7 | ulid = ULID.generate 8 | 9 | assert_equal ulid.length, 26 10 | end 11 | 12 | it 'is sortable' do 13 | input_time = Time.now 14 | ulid1 = ULID.generate(input_time) 15 | ulid2 = ULID.generate(input_time + 1) 16 | assert ulid2 > ulid1 17 | end 18 | 19 | it 'is valid Crockford Base32' do 20 | ulid = ULID.generate 21 | decoded = Base32::Crockford.decode(ulid) 22 | encoded = Base32::Crockford.encode(decoded, length: 26) 23 | assert_equal ulid, encoded 24 | end 25 | 26 | it 'encodes the timestamp in the first 10 characters' do 27 | # test case taken from original ulid README: 28 | # https://github.com/ulid/javascript#seed-time 29 | # 30 | # N.b. we avoid specifying the time as a float, since we lose precision: 31 | # 32 | # > Time.at(1_469_918_176.385).strftime("%F %T.%N") 33 | # => "2016-07-30 23:36:16.384999990" 34 | # 35 | # vs the correct: 36 | # 37 | # > Time.at(1_469_918_176, 385, :millisecond).strftime("%F %T.%N") 38 | # => "2016-07-30 23:36:16.385000000" 39 | ulid = ULID.generate(Time.at(1_469_918_176, 385, :millisecond)) 40 | assert_equal '01ARYZ6S41', ulid[0...10] 41 | end 42 | 43 | it 'respects millisecond-precision order' do 44 | ulids = Array.new(1000) do |millis| 45 | time = Time.new(2020, 1, 2, 3, 4, Rational(millis, 10**3)) 46 | 47 | ULID.generate(time) 48 | end 49 | 50 | assert_equal(ulids, ulids.sort) 51 | end 52 | 53 | it 'is deterministic based on time' do 54 | input_time = Time.now 55 | ulid1 = ULID.generate(input_time) 56 | ulid2 = ULID.generate(input_time) 57 | assert_equal ulid2.slice(0, 10), ulid1.slice(0, 10) 58 | assert ulid2 != ulid1 59 | end 60 | 61 | it 'is deterministic based on suffix' do 62 | input_time = Time.now 63 | suffix = SecureRandom.uuid 64 | ulid1 = ULID.generate(input_time, suffix: suffix) 65 | ulid2 = ULID.generate(input_time + 1, suffix: suffix) 66 | assert_equal ulid2.slice(10, 26), ulid1.slice(10, 26) 67 | assert ulid2 != ulid1 68 | end 69 | 70 | it 'is fully deterministic based on time and suffix' do 71 | input_time = Time.now 72 | suffix = SecureRandom.uuid 73 | ulid1 = ULID.generate(input_time, suffix: suffix) 74 | ulid2 = ULID.generate(input_time, suffix: suffix) 75 | assert_equal ulid2, ulid1 76 | end 77 | 78 | it 'raises exception when non-encodable 80-bit suffix string is used' do 79 | input_time = Time.now 80 | suffix = SecureRandom.uuid 81 | assert_raises(ArgumentError) do 82 | ULID.generate(input_time, suffix: suffix[0...9]) 83 | end 84 | 85 | ULID.generate(input_time, suffix: suffix[0...10]) 86 | end 87 | end 88 | 89 | describe 'underlying binary' do 90 | it 'encodes the timestamp in the high 48 bits' do 91 | input_time = Time.now.utc 92 | bytes = ULID.generate_bytes(input_time) 93 | (time_ms,) = "\x0\x0#{bytes[0...6]}".unpack('Q>') 94 | encoded_time = Time.at(time_ms / 1000.0).utc 95 | assert_in_delta input_time, encoded_time, 0.001 96 | end 97 | 98 | it 'encodes the remaining 80 bits as random' do 99 | random_bytes = SecureRandom.random_bytes(ULID::Generator::RANDOM_BITS / 8) 100 | SecureRandom.stub(:random_bytes, random_bytes) do 101 | bytes = ULID.generate_bytes 102 | assert bytes[6..-1] == random_bytes 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚨 Please hold on 2 | 3 | **If you're starting a new project with a empty database, use UUID v7 instead.** 4 | 5 | **If you’re trying to address database slowness caused by non-time-ordered UUIDs, such as UUIDv4, use UUIDv7 instead.** 6 | 7 | ➡️ Read more at: 8 | - https://uuid7.com 9 | - https://buildkite.com/resources/blog/goodbye-integers-hello-uuids/ 10 | 11 | --- 12 | 13 |  14 | [](https://rubygems.org/gems/ulid) 15 | [](https://github.com/rafaelsales/ulid) 16 | 17 | # ulid 18 | Universally Unique Lexicographically Sortable Identifier implementation for Ruby 19 | 20 | Official specification page: https://github.com/ulid/spec 21 | 22 |
26 |