├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── examples └── bench.rb ├── lib ├── uuid7.rb └── uuid7 │ ├── generator.rb │ └── version.rb ├── spec ├── spec_helper.rb ├── uuid7 │ └── generator_spec.rb └── uuid7_spec.rb └── uuid7.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | *.gem 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | TargetRubyVersion: 2.6 4 | 5 | Style/StringLiterals: 6 | Enabled: true 7 | EnforcedStyle: double_quotes 8 | 9 | Style/StringLiteralsInInterpolation: 10 | Enabled: true 11 | EnforcedStyle: double_quotes 12 | 13 | Style/FormatStringToken: 14 | Enabled: false 15 | 16 | Metrics/MethodLength: 17 | Enabled: false 18 | 19 | Metrics/BlockLength: 20 | Enabled: false 21 | 22 | Layout/LineLength: 23 | Max: 120 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in uuid7.gemspec 6 | gemspec 7 | 8 | gem "guard-rspec" 9 | gem "rake", "~> 13.0" 10 | gem "rspec", "~> 3.0" 11 | gem "rubocop", "~> 1.7" 12 | gem "rubocop-rake" 13 | gem "rubocop-rspec" 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | uuid7 (0.2.0) 5 | zeitwerk (~> 2.4) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ast (2.4.2) 11 | coderay (1.1.3) 12 | diff-lcs (1.5.0) 13 | ffi (1.15.5) 14 | formatador (1.1.0) 15 | guard (2.18.0) 16 | formatador (>= 0.2.4) 17 | listen (>= 2.7, < 4.0) 18 | lumberjack (>= 1.0.12, < 2.0) 19 | nenv (~> 0.1) 20 | notiffany (~> 0.0) 21 | pry (>= 0.13.0) 22 | shellany (~> 0.0) 23 | thor (>= 0.18.1) 24 | guard-compat (1.2.1) 25 | guard-rspec (4.7.3) 26 | guard (~> 2.1) 27 | guard-compat (~> 1.1) 28 | rspec (>= 2.99.0, < 4.0) 29 | listen (3.7.1) 30 | rb-fsevent (~> 0.10, >= 0.10.3) 31 | rb-inotify (~> 0.9, >= 0.9.10) 32 | lumberjack (1.2.8) 33 | method_source (1.0.0) 34 | nenv (0.3.0) 35 | notiffany (0.1.3) 36 | nenv (~> 0.1) 37 | shellany (~> 0.0) 38 | parallel (1.22.1) 39 | parser (3.1.2.0) 40 | ast (~> 2.4.1) 41 | pry (0.14.1) 42 | coderay (~> 1.1) 43 | method_source (~> 1.0) 44 | rainbow (3.1.1) 45 | rake (13.0.6) 46 | rb-fsevent (0.11.1) 47 | rb-inotify (0.10.1) 48 | ffi (~> 1.0) 49 | regexp_parser (2.4.0) 50 | rexml (3.2.5) 51 | rspec (3.11.0) 52 | rspec-core (~> 3.11.0) 53 | rspec-expectations (~> 3.11.0) 54 | rspec-mocks (~> 3.11.0) 55 | rspec-core (3.11.0) 56 | rspec-support (~> 3.11.0) 57 | rspec-expectations (3.11.0) 58 | diff-lcs (>= 1.2.0, < 2.0) 59 | rspec-support (~> 3.11.0) 60 | rspec-mocks (3.11.1) 61 | diff-lcs (>= 1.2.0, < 2.0) 62 | rspec-support (~> 3.11.0) 63 | rspec-support (3.11.0) 64 | rubocop (1.29.0) 65 | parallel (~> 1.10) 66 | parser (>= 3.1.0.0) 67 | rainbow (>= 2.2.2, < 4.0) 68 | regexp_parser (>= 1.8, < 3.0) 69 | rexml (>= 3.2.5, < 4.0) 70 | rubocop-ast (>= 1.17.0, < 2.0) 71 | ruby-progressbar (~> 1.7) 72 | unicode-display_width (>= 1.4.0, < 3.0) 73 | rubocop-ast (1.17.0) 74 | parser (>= 3.1.1.0) 75 | rubocop-rake (0.6.0) 76 | rubocop (~> 1.0) 77 | rubocop-rspec (2.10.0) 78 | rubocop (~> 1.19) 79 | ruby-progressbar (1.11.0) 80 | shellany (0.0.1) 81 | thor (1.2.1) 82 | unicode-display_width (2.1.0) 83 | zeitwerk (2.5.4) 84 | 85 | PLATFORMS 86 | x86_64-darwin-20 87 | x86_64-darwin-21 88 | 89 | DEPENDENCIES 90 | guard-rspec 91 | rake (~> 13.0) 92 | rspec (~> 3.0) 93 | rubocop (~> 1.7) 94 | rubocop-rake 95 | rubocop-rspec 96 | uuid7! 97 | 98 | BUNDLED WITH 99 | 2.2.15 100 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A sample Guardfile 4 | # More info at https://github.com/guard/guard#readme 5 | 6 | ## Uncomment and set this to only include directories you want to watch 7 | # directories %w(app lib config test spec features) \ 8 | # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")} 9 | 10 | ## Note: if you are using the `directories` clause above and you are not 11 | ## watching the project directory ('.'), then you will want to move 12 | ## the Guardfile to a watched dir and symlink it back, e.g. 13 | # 14 | # $ mkdir config 15 | # $ mv Guardfile config/ 16 | # $ ln -s config/Guardfile . 17 | # 18 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 19 | 20 | # NOTE: The cmd option is now required due to the increasing number of ways 21 | # rspec may be run, below are examples of the most common uses. 22 | # * bundler: 'bundle exec rspec' 23 | # * bundler binstubs: 'bin/rspec' 24 | # * spring: 'bin/rspec' (This will use spring if running and you have 25 | # installed the spring binstubs per the docs) 26 | # * zeus: 'zeus rspec' (requires the server to be started separately) 27 | # * 'just' rspec: 'rspec' 28 | 29 | guard :rspec, cmd: "bundle exec rspec" do 30 | require "guard/rspec/dsl" 31 | dsl = Guard::RSpec::Dsl.new(self) 32 | 33 | # Feel free to open issues for suggestions and improvements 34 | 35 | # RSpec files 36 | rspec = dsl.rspec 37 | watch(rspec.spec_helper) { rspec.spec_dir } 38 | watch(rspec.spec_support) { rspec.spec_dir } 39 | watch(rspec.spec_files) 40 | 41 | # Ruby files 42 | ruby = dsl.ruby 43 | dsl.watch_spec_files_for(ruby.lib_files) 44 | 45 | # Rails files 46 | rails = dsl.rails(view_extensions: %w[erb haml slim]) 47 | dsl.watch_spec_files_for(rails.app_files) 48 | dsl.watch_spec_files_for(rails.views) 49 | 50 | watch(rails.controllers) do |m| 51 | [ 52 | rspec.spec.call("routing/#{m[1]}_routing"), 53 | rspec.spec.call("controllers/#{m[1]}_controller"), 54 | rspec.spec.call("acceptance/#{m[1]}") 55 | ] 56 | end 57 | 58 | # Rails config changes 59 | watch(rails.spec_helper) { rspec.spec_dir } 60 | watch(rails.routes) { "#{rspec.spec_dir}/routing" } 61 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } 62 | 63 | # Capybara features specs 64 | watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } 65 | watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } 66 | 67 | # Turnip features and steps 68 | watch(%r{^spec/acceptance/(.+)\.feature$}) 69 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| 70 | Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2021 Alexander Obukhov 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UUID7 2 | 3 | Implementation of [UUIDv7](https://github.com/uuid6/uuid6-ietf-draft) in [Ruby](https://www.ruby-lang.org/). 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'uuid7' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle install 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install uuid7 20 | 21 | ## Usage 22 | 23 | ``` 24 | UUID7.generate 25 | ``` 26 | 27 | ## Performance 28 | 29 | | Library | Iterations/second | 30 | |--------:|----------------------:| 31 | | UUIDv7 | 412.324k (± 0.5%) i/s | 32 | | UUIDv4 | 483.110k (± 0.7%) i/s | 33 | | ULID | 156.675k (± 0.7%) i/s | 34 | | KSUID | 108.075k (± 0.9%) i/s | 35 | 36 | See [examples](https://github.com/sprql/uuid7-ruby/examples/bench.rb) 37 | 38 | ## Development 39 | 40 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 41 | 42 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 43 | 44 | ## Contributing 45 | 46 | Bug reports and pull requests are welcome on GitHub at https://github.com/sprql/uuid7-ruby. 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | require "rubocop/rake_task" 9 | 10 | RuboCop::RakeTask.new 11 | 12 | task default: %i[spec rubocop] 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "uuid7" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/bench.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/inline" 4 | 5 | gemfile do 6 | source "https://rubygems.org" 7 | 8 | gem "benchmark-ips" 9 | gem "kalibera" 10 | 11 | gem "uuid7" 12 | gem "ulid" 13 | gem "ksuid" 14 | end 15 | 16 | require "benchmark/ips" 17 | 18 | Benchmark.ips do |x| 19 | x.time = 10 20 | x.stats = :bootstrap 21 | 22 | x.report("UUIDv7") { UUID7.generate } 23 | x.report("UUIDv4") { SecureRandom.uuid } 24 | x.report("ULID") { ULID.generate } 25 | x.report("KSUID") { KSUID.new } 26 | 27 | x.compare! 28 | end 29 | -------------------------------------------------------------------------------- /lib/uuid7.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "zeitwerk" 4 | loader = Zeitwerk::Loader.for_gem 5 | loader.inflector.inflect("uuid7" => "UUID7") 6 | loader.setup 7 | 8 | # UUID v7 Generator 9 | # 10 | module UUID7 11 | # Generate a new UUID v7 12 | # 13 | # @api public 14 | # 15 | # @example Generate a new UUID v7 for the current timestamp 16 | # UUID7.generate 17 | # 18 | # @param timestamp [Integer] the timestamp to use for UUID v7 19 | # @return [String] the generated UUID v7 string 20 | def self.generate(timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)) 21 | format("%08x-%04x-%04x-%04x-%04x%08x", *Generator.generate(timestamp)) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/uuid7/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UUID7 4 | # UUIDv7 with millisecond precision layout 5 | # 6 | # @api private 7 | # 8 | # 48 bits for unix timestamp with millisecond precision 9 | # 74 bits for random data 10 | # 11 | module Generator 12 | VERSION_7 = 0x7000 13 | VARIANT_RFC4122 = 0x8000 14 | 15 | # Instantiates a new layout 16 | # 17 | # @example Generate a new UUID v7 layout for the millisecond 18 | # UUID::V7.generate(timestamp) 19 | # 20 | # @param timestamp [Integer] the timestamp to use for the layout 21 | # @return [Array] the generated UUID v7 layout 22 | def self.generate(timestamp) 23 | unix_ts_ms = timestamp & 0xffffffffffff # take 48 least significant bits of timestamp 24 | unix_ts_ms1 = (unix_ts_ms >> 16) & 0xffffffff # take 32 most significant bits of timestamp 25 | unix_ts_ms2 = (unix_ts_ms & 0xffff) # take 16 least significant bits of timestamp 26 | 27 | rand_a, rand_b1, rand_b2, rand_b3 = SecureRandom.gen_random(10).unpack("nnnN") 28 | rand_a &= 0xfff # take 12 bits of 16 29 | rand_b1 &= 0x3fff # take 14 bits of 16 30 | 31 | # https://www.ietf.org/id/draft-peabody-dispatch-new-uuid-format-03.txt#section-5.2 32 | # 0 1 2 3 33 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 34 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 35 | # | unix_ts_ms | 36 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 37 | # | unix_ts_ms | ver | rand_a | 38 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 39 | # |var| rand_b | 40 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 41 | # | rand_b | 42 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 43 | 44 | # layout in bits [32, 16, 16, 16, 16, 32] 45 | [ 46 | unix_ts_ms1, 47 | unix_ts_ms2, 48 | (VERSION_7 | rand_a), 49 | (VARIANT_RFC4122 | rand_b1), 50 | rand_b2, 51 | rand_b3 52 | ] 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/uuid7/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UUID7 4 | VERSION = "0.2.0" 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uuid7" 4 | 5 | module UUID7SpecHelpers 6 | def make_timestamp 7 | Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) 8 | end 9 | end 10 | 11 | RSpec.configure do |config| 12 | # Enable flags like --only-failures and --next-failure 13 | config.example_status_persistence_file_path = ".rspec_status" 14 | 15 | # Disable RSpec exposing methods globally on `Module` and `main` 16 | config.disable_monkey_patching! 17 | 18 | config.expect_with :rspec do |c| 19 | c.syntax = :expect 20 | end 21 | 22 | config.include UUID7SpecHelpers 23 | end 24 | -------------------------------------------------------------------------------- /spec/uuid7/generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe UUID7::Generator do 4 | context "generate UUID v7 layout" do 5 | let(:timestamp) { make_timestamp } 6 | 7 | subject { described_class.generate(timestamp) } 8 | 9 | it do 10 | expect(subject).to contain_exactly(be_a(Integer), 11 | be_a(Integer), 12 | be_a(Integer), 13 | be_a(Integer), 14 | be_a(Integer), 15 | be_a(Integer)) 16 | end 17 | end 18 | 19 | context "generate different layouts" do 20 | let(:timestamp) { make_timestamp } 21 | 22 | def new_uuid7 23 | described_class.generate(timestamp) 24 | end 25 | 26 | it do 27 | expect(new_uuid7).not_to eq(new_uuid7) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/uuid7_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe UUID7 do 4 | it "has a version number" do 5 | expect(UUID7::VERSION).not_to be nil 6 | end 7 | 8 | it "generate a UUID v7 string" do 9 | expect(described_class.generate).to match(/\w{8}-\w{4}-7\w{3}-\w{4}-\w{4}\w{8}/) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /uuid7.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/uuid7/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "uuid7" 7 | spec.version = UUID7::VERSION 8 | spec.authors = ["Alexander Obukhov"] 9 | spec.email = ["dev@sprql.space"] 10 | 11 | spec.summary = "UUID v7 generator" 12 | spec.description = "Implementation of UUID v7" 13 | spec.homepage = "https://github.com/sprql/uuid7-ruby" 14 | spec.licenses = ["MIT"] 15 | spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0") 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | spec.files = Dir.glob("{lib}/**/*") + %w[LICENSE README.md] 22 | spec.bindir = "bin" 23 | spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) } 24 | spec.require_paths = ["lib"] 25 | 26 | # Uncomment to register a new dependency of your gem 27 | spec.add_dependency "zeitwerk", "~> 2.4" 28 | 29 | # For more information and examples about making a new gem, checkout our 30 | # guide at: https://bundler.io/guides/creating_gem.html 31 | end 32 | --------------------------------------------------------------------------------