├── .editorconfig ├── .envrc ├── .github ├── dependabot.yml ├── renovate.json └── workflows │ ├── ci-nix.yml │ ├── ci.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── lint.yml │ ├── merge-bot-pr.yml │ ├── pages.yml │ ├── release.yml │ ├── signature.yml │ └── update-nixpkgs-and-versions-in-ci.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── .vscode ├── extensions.json └── settings.json ├── .yardopts ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── Steepfile ├── _typos.toml ├── assets └── logo.png ├── benchmark ├── compare_with_othergems │ ├── abachman │ │ ├── Gemfile │ │ ├── generate.rb │ │ └── parser.rb │ ├── kachick │ │ ├── Gemfile │ │ ├── generate.rb │ │ └── parser.rb │ └── rafaelsales │ │ ├── Gemfile │ │ ├── generate.rb │ │ └── parser.rb ├── core_instance_methods.rb ├── generate_vs_encode.rb ├── monotonic_generators_with_different_time.rb ├── monotonic_generators_with_now.rb ├── parsers.rb ├── sample.rb ├── sort.rb └── tools.rb ├── bin └── console ├── dprint.json ├── examples └── rbs_sandbox │ ├── .vscode │ └── extensions.json │ ├── Gemfile │ ├── Steepfile │ └── lib │ └── my_sandbox.rb ├── flake.lock ├── flake.nix ├── lib ├── ruby-ulid.rb ├── ulid.rb └── ulid │ ├── crockford_base32.rb │ ├── errors.rb │ ├── monotonic_generator.rb │ ├── utils.rb │ ├── uuid.rb │ ├── uuid │ └── fields.rb │ └── version.rb ├── ruby-ulid.gemspec ├── scripts ├── generate_snapshots.rb └── prof.rb ├── sig └── ulid.rbs ├── steep_expectations.yml └── test ├── concurrency ├── test_generators_in_ractor.rb └── test_ulid_monotonic_generator_thread_safety.rb ├── core ├── test_boundary_ulid.rb ├── test_library_loading.rb ├── test_ractor_shareable.rb ├── test_ulid_class.rb ├── test_ulid_example_values.rb ├── test_ulid_instance.rb ├── test_ulid_monotonic_generator.rb ├── test_ulid_subclass.rb ├── test_ulid_usecase.rb └── test_uuid.rb ├── helper.rb ├── longtime ├── test_monotonic_generator_with_no_moment_depends_on_current_time.rb └── test_ulid_monotonic_generator_single_thread.rb └── many_data ├── fixtures └── snapshots_2024-03-20_10-18.toml ├── test_randomized_many_data.rb ├── test_snapshots.rb └── test_ulid_sample_with_many_data.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | use flake 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | ignore: 8 | - dependency-name: 'crate-ci/typos' 9 | groups: 10 | determinatesystems-actions: 11 | patterns: 12 | - 'DeterminateSystems*' 13 | 14 | - package-ecosystem: 'bundler' 15 | directory: '/' 16 | schedule: 17 | interval: 'weekly' 18 | versioning-strategy: increase 19 | groups: 20 | rubocop-dependencies: 21 | patterns: 22 | - '*rubocop*' 23 | rbs-dependencies: 24 | patterns: 25 | - 'rbs*' 26 | - '*steep*' 27 | ignore: 28 | - dependency-name: 'rubocop' 29 | versions: 30 | # https://github.com/rubocop/rubocop/pull/10796 31 | - '1.31.2' 32 | # https://github.com/rubocop/rubocop/issues/11549 33 | - '1.45.0' 34 | # https://github.com/rubocop/rubocop/pull/14067#issuecomment-2820741234 35 | - '1.75.3' 36 | - package-ecosystem: 'bundler' 37 | directory: '/benchmark/compare_with_othergems/kachick' 38 | schedule: 39 | interval: 'weekly' 40 | versioning-strategy: increase 41 | - package-ecosystem: 'bundler' 42 | directory: '/benchmark/compare_with_othergems/rafaelsales' 43 | schedule: 44 | interval: 'weekly' 45 | versioning-strategy: increase 46 | - package-ecosystem: 'bundler' 47 | directory: '/benchmark/compare_with_othergems/abachman' 48 | schedule: 49 | interval: 'weekly' 50 | versioning-strategy: increase 51 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "labels": ["dependencies", "renovate"], 4 | "enabledManagers": ["nix", "regex"], 5 | "extends": [ 6 | "github>kachick/renovate-config-dprint#1.3.0", 7 | "github>kachick/renovate-config-dprint:self" 8 | ], 9 | "nix": { 10 | "enabled": true 11 | }, 12 | "schedule": ["on Tuesday"] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/ci-nix.yml: -------------------------------------------------------------------------------- 1 | name: CI - Nix 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - '.github/workflows/ci-nix.yml' 7 | - '**.nix' 8 | - 'flake.*' 9 | - 'Gemfile*' 10 | pull_request: 11 | paths: 12 | - '.github/workflows/ci-nix.yml' 13 | - '**.nix' 14 | - 'flake.*' 15 | - 'Gemfile*' 16 | schedule: 17 | # Every 10:42 JST 18 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule 19 | - cron: '42 1 * * *' 20 | workflow_dispatch: 21 | 22 | jobs: 23 | tasks: 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | os: 28 | - ubuntu-24.04 29 | runs-on: ${{ matrix.os }} 30 | timeout-minutes: 30 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: DeterminateSystems/nix-installer-action@v17 34 | - run: nix flake check 35 | - run: nix develop --command echo 'This step should be done before any other "nix develop" steps because of measuring Nix build time' 36 | - run: nix run .#ruby -- -rprime -e 'p Prime.first 5; p ULID.generate' 37 | - run: nix develop --command bundle install 38 | - name: Log current versions 39 | run: nix develop --command bundle exec rake deps 40 | - name: Test with external dependencies 41 | run: nix develop --command bundle exec rake check_non_ruby 42 | - name: Test with Ruby 43 | run: nix develop --command bundle exec rake 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions 2 | name: CI 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | branches: 8 | - main 9 | paths: 10 | - '.github/workflows/ci.yml' 11 | - 'lib/**' 12 | - 'test/**' 13 | - 'spec/**' 14 | - '.rspec' 15 | - '**.gemspec' 16 | - 'Gemfile' 17 | - 'Rakefile' 18 | - '.ruby-version' 19 | pull_request: 20 | paths: 21 | - '.github/workflows/ci.yml' 22 | - 'lib/**' 23 | - 'test/**' 24 | - 'spec/**' 25 | - '.rspec' 26 | - '**.gemspec' 27 | - 'Gemfile' 28 | - 'Rakefile' 29 | - '.ruby-version' 30 | 31 | jobs: 32 | test: 33 | timeout-minutes: 15 34 | strategy: 35 | fail-fast: false 36 | # Syntax https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs 37 | matrix: 38 | # macos is covered in nix ci 39 | os: ['ubuntu-24.04'] 40 | # Due to https://github.com/actions/runner/issues/849, we have to use quotes for 'n.0' 41 | ruby: ['head', '3.4', '3.3'] 42 | runs-on: ${{ matrix.os }} 43 | env: 44 | # https://github.com/kachick/ruby-ulid/blob/104834846baf5caa1e8536a11c43acdd56fc849c/CONTRIBUTING.md#adding-dependencies-for-this-gem 45 | BUNDLE_WITHOUT: development 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 49 | with: 50 | ruby-version: ${{ matrix.ruby }} 51 | # Enabling is the recommended way, but it cannot detect runner changes in early stage. 52 | # So disable it is better for test job, do not mind in other jobs 53 | bundler-cache: false # runs 'bundle install' and caches installed gems automatically 54 | - run: bundle install 55 | - run: bundle exec rake test_all --suppress-backtrace='\/bundle' 56 | - run: bundle exec rake validate_gem 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: ['main'] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ['main'] 20 | schedule: 21 | - cron: '23 18 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-24.04 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['ruby'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v3 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 58 | # If this step fails, then you should remove it and run the build manually (see below) 59 | - name: Autobuild 60 | uses: github/codeql-action/autobuild@v3 61 | 62 | # ℹ️ Command-line programs to run using the OS shell. 63 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 64 | 65 | # If the Autobuild fails above, remove it and uncomment the following three lines. 66 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 67 | 68 | # - run: | 69 | # echo "Run, Build Application using script" 70 | # ./location_of_script_within_repo/buildscript.sh 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v3 74 | with: 75 | category: '/language:${{matrix.language}}' 76 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | on: 3 | pull_request: 4 | paths: 5 | - '.github/workflows/dependency-review.yml' 6 | - 'Gemfile' 7 | - '**.nix' 8 | - 'flake.*' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - name: 'Checkout Repository' 19 | uses: actions/checkout@v4 20 | - name: 'Dependency Review' 21 | uses: actions/dependency-review-action@v4 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions 2 | name: Lint 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'docs/**' 9 | - '.vscode/**' 10 | pull_request: null # This enables to run on each PRs 11 | jobs: 12 | rubocop: 13 | timeout-minutes: 15 14 | runs-on: 'ubuntu-24.04' 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 18 | with: 19 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 20 | - run: bundle exec rake rubocop 21 | 22 | dprint: 23 | timeout-minutes: 15 24 | runs-on: ubuntu-24.04 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: dprint/check@v2.2 28 | with: 29 | dprint-version: '0.49.1' # selfup {"extract":"\\d[^']+","replacer":["bash","-c","dprint --version | cut -d ' ' -f 2"]} 30 | 31 | typos: 32 | timeout-minutes: 15 33 | runs-on: ubuntu-24.04 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: crate-ci/typos@v1.32.0 # selfup {"extract":"\\d\\.\\d+\\.\\d+","replacer":["bash","-c","typos --version | cut -d ' ' -f 2"]} 37 | with: 38 | # https://github.com/crate-ci/typos/issues/779 39 | files: | 40 | . 41 | .github 42 | .vscode 43 | -------------------------------------------------------------------------------- /.github/workflows/merge-bot-pr.yml: -------------------------------------------------------------------------------- 1 | name: Merge bot PR after CI 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | # checks: read # For private repositories 8 | # actions: read # For private repositories 9 | 10 | jobs: 11 | dependabot: 12 | timeout-minutes: 30 13 | runs-on: ubuntu-24.04 14 | if: ${{ github.actor == 'dependabot[bot]' }} 15 | steps: 16 | - name: Dependabot metadata 17 | id: metadata 18 | uses: dependabot/fetch-metadata@v2.4.0 19 | - name: Wait other jobs 20 | if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }} 21 | uses: kachick/wait-other-jobs@v3.8.1 22 | timeout-minutes: 10 23 | with: 24 | retry-method: 'equal_intervals' 25 | min-interval-seconds: '15' 26 | skip-same-workflow: 'true' 27 | - name: Approve and merge 28 | if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' || contains(steps.metadata.outputs.dependency-names, 'DeterminateSystems') }} 29 | run: gh pr review --approve "$PR_URL" && gh pr merge --auto --squash "$PR_URL" 30 | env: 31 | PR_URL: ${{ github.event.pull_request.html_url }} 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | # Avoid `automerge` renovate official feature. 35 | # It wait longtime to be merged. 36 | # Avoid `platformAutomerge` renovate official feature. 37 | # It requires many changes in GitHub settings. 38 | # - `Allow auto-merge` 39 | # - `Require status checks to pass before merging` and specify the status names 40 | # Changing in all personal repository is annoy task for me. Even if using terrafform, getting mandatory CI names in each repo is too annoy! 41 | renovate: 42 | timeout-minutes: 30 43 | runs-on: ubuntu-24.04 44 | if: ${{ github.actor == 'renovate[bot]' }} 45 | steps: 46 | - name: Wait other jobs 47 | uses: kachick/wait-other-jobs@v3.8.1 48 | timeout-minutes: 10 49 | with: 50 | skip-same-workflow: 'true' 51 | - name: Approve and merge 52 | run: gh pr review --approve "$PR_URL" && gh pr merge --auto --squash "$PR_URL" 53 | env: 54 | PR_URL: ${{ github.event.pull_request.html_url }} 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | # https://github.com/kachick/anylang-template/issues/51 58 | selfup-runner: 59 | timeout-minutes: 30 60 | runs-on: ubuntu-24.04 61 | if: ${{ github.actor == 'selfup-runner[bot]' }} 62 | steps: 63 | - name: Wait other jobs 64 | uses: kachick/wait-other-jobs@v3.8.1 65 | timeout-minutes: 20 66 | with: 67 | skip-same-workflow: 'true' 68 | - name: Approve and merge 69 | run: gh pr review --approve "$PR_URL" && gh pr merge --auto --delete-branch --squash "$PR_URL" 70 | env: 71 | PR_URL: ${{ github.event.pull_request.html_url }} 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy API docs to GitHub Pages 2 | on: 3 | push: 4 | branches: ['main'] 5 | paths: 6 | - '.github/workflows/pages.yml' 7 | - '.yardopts' 8 | - 'lib/**' 9 | - '**.gemspec' 10 | - 'Gemfile' 11 | - 'README.md' 12 | - '**.txt' 13 | - '.ruby-version' 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | permissions: 18 | contents: read 19 | pages: write 20 | id-token: write 21 | 22 | # Allow one concurrent deployment 23 | concurrency: 24 | group: 'pages' 25 | cancel-in-progress: true 26 | 27 | jobs: 28 | deploy_yard: 29 | timeout-minutes: 15 30 | environment: 31 | name: github-pages 32 | url: ${{ steps.deployment.outputs.page_url }} 33 | runs-on: ubuntu-24.04 34 | name: Build and deploy YARD 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 39 | with: 40 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 41 | - run: bundle exec yard 42 | shell: bash 43 | - uses: actions/configure-pages@v5 44 | - uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: doc 47 | - id: deployment 48 | uses: actions/deploy-pages@v4 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # See GH-544 for detail 2 | name: 🚀 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | branches: 8 | - main 9 | paths: 10 | - '.github/workflows/release.yml' 11 | - 'lib/**' 12 | - '**.gemspec' 13 | - 'Gemfile' 14 | - 'Rakefile' 15 | - '.ruby-version' 16 | pull_request: 17 | paths: 18 | - '.github/workflows/release.yml' 19 | - 'lib/**' 20 | - '**.gemspec' 21 | - 'Gemfile' 22 | - 'Rakefile' 23 | - '.ruby-version' 24 | workflow_dispatch: 25 | jobs: 26 | build: 27 | timeout-minutes: 15 28 | runs-on: 'ubuntu-24.04' 29 | env: 30 | # https://github.com/kachick/ruby-ulid/blob/104834846baf5caa1e8536a11c43acdd56fc849c/CONTRIBUTING.md#adding-dependencies-for-this-gem 31 | BUNDLE_WITHOUT: development 32 | outputs: 33 | gem_file: ${{ steps.build.outputs.built_file }} 34 | checksum_file: ${{ steps.isnpect.outputs.checksum_file }} 35 | prerelease: ${{ steps.isnpect.outputs.prerelease }} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 39 | with: 40 | # Enabling is the recommended way, but it cannot detect runner changes in early stage. 41 | # So disable it is better for test job, do not mind in other jobs 42 | bundler-cache: false # runs 'bundle install' and caches installed gems automatically 43 | - run: bundle install 44 | - name: Build 45 | id: build 46 | run: | 47 | built_file="$(bundle exec rake validate_gem | ruby -e 'puts STDIN.read.slice(/\bFile: (ruby-ulid-\S+?\.gem)$/, 1)')" 48 | package_name="$(basename --suffix '.gem' "$built_file")" 49 | echo "built_file=$built_file" | tee --append "$GITHUB_OUTPUT" 50 | echo "package_name=$package_name" | tee --append "$GITHUB_OUTPUT" 51 | - name: Inspect 52 | id: isnpect 53 | run: | 54 | gem install '${{ steps.build.outputs.built_file }}' 55 | gem unpack '${{ steps.build.outputs.built_file }}' 56 | tree '${{ steps.build.outputs.package_name }}' 57 | checksum_file="${{ steps.build.outputs.package_name }}_checksums.txt" 58 | sha256sum '${{ steps.build.outputs.built_file }}' | tee --append "$checksum_file" 59 | echo "checksum_file=$checksum_file" | tee --append "$GITHUB_OUTPUT" 60 | if [[ ${GITHUB_REF#refs/tags/} =~ ^v[0-9]+\.[0-9]+\.[0-9]+.+ ]]; then 61 | echo 'prerelease=true' >> "$GITHUB_OUTPUT" 62 | else 63 | echo 'prerelease=false' >> "$GITHUB_OUTPUT" 64 | fi 65 | - name: Upload the gem file as an artifact 66 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 67 | with: 68 | name: 'release-assets' 69 | path: | 70 | ${{ steps.build.outputs.built_file }} 71 | ${{ steps.isnpect.outputs.checksum_file }} 72 | check-installability: 73 | needs: [build] 74 | timeout-minutes: 15 75 | strategy: 76 | fail-fast: false 77 | # Syntax https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs 78 | matrix: 79 | os: 80 | - ubuntu-24.04 81 | - macos-15 # aarch64 82 | # - macos-13 # x86_64 - CPU arch does not affect for this gem (I think) 83 | # - windows-2022 - Too slow. Please tell me if any Windows user is using this gem. 84 | # For actual use-case, head is needless here 85 | ruby: ['3.4', '3.3'] 86 | runs-on: ${{ matrix.os }} 87 | steps: 88 | # Required to checkout for gh command 89 | - uses: actions/checkout@v4 90 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 91 | with: 92 | ruby-version: ${{ matrix.ruby }} 93 | bundler-cache: false 94 | - name: Download release-assets 95 | env: 96 | GH_TOKEN: ${{ github.token }} 97 | # Do not use tree and 'tee --append; for macos and windows runner. Keep minimum 98 | run: | 99 | gh run download '${{ github.run_id }}' 100 | - name: Make sure we can use the gem 101 | run: | 102 | gem install 'release-assets/${{ needs.build.outputs.gem_file }}' 103 | ruby -r 'ulid' -e 'pp [ULID.generate, ULID.sample(3), ULID.parse(%q_01F4A5Y1YAQCYAYCTC7GRMJ9AA_)]' 104 | github: 105 | if: startsWith(github.ref, 'refs/tags/') 106 | needs: [build, check-installability] 107 | timeout-minutes: 15 108 | runs-on: 'ubuntu-24.04' 109 | env: 110 | GH_TOKEN: ${{ github.token }} 111 | steps: 112 | # Required to checkout for gh command 113 | - uses: actions/checkout@v4 114 | - name: Download release-assets 115 | run: | 116 | gh run download '${{ github.run_id }}' 117 | tree release-assets 118 | - name: Wait other jobs 119 | uses: kachick/wait-other-jobs@v3.8.1 120 | timeout-minutes: 10 121 | with: 122 | skip-same-workflow: 'true' 123 | skip-list: | 124 | [ 125 | { 126 | "workflowFile": "merge-bot-pr.yml" 127 | } 128 | ] 129 | - name: Publish as a prerelease version 130 | # To be strict, prefer `!= false` than `== true` 131 | if: ${{ needs.build.outputs.prerelease != 'false' }} 132 | run: | 133 | gh release create --verify-tag "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --prerelease release-assets/* 134 | - name: Publish 135 | if: ${{ needs.build.outputs.prerelease == 'false' }} 136 | run: | 137 | gh release create --verify-tag "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" release-assets/* 138 | rubygems: 139 | if: startsWith(github.ref, 'refs/tags/') 140 | needs: [build, check-installability, github] 141 | timeout-minutes: 15 142 | runs-on: 'ubuntu-24.04' 143 | permissions: 144 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 145 | # https://github.com/kachick/ruby-ulid/deployments/release 146 | # https://github.com/kachick/ruby-ulid/settings/environments 147 | environment: release 148 | steps: 149 | # Required to checkout for gh command 150 | - uses: actions/checkout@v4 151 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 152 | with: 153 | bundler-cache: false 154 | - name: Download release-assets 155 | env: 156 | GH_TOKEN: ${{ github.token }} 157 | run: | 158 | gh run download '${{ github.run_id }}' 159 | tree release-assets 160 | - name: Configure trusted publishing credentials 161 | uses: rubygems/configure-rubygems-credentials@bc6dd217f8a4f919d6835fcfefd470ef821f5c44 # v1.0.0 162 | # with: 163 | # https://github.com/rubygems/configure-rubygems-credentials/blob/ca0ef5249c429db0cfc96ce44475ad2e6f4da260/README.md#L84-L93 164 | # TODO: Update after we got reply in https://github.com/rubygems/configure-rubygems-credentials/issues/161 165 | # role-to-assume: 3 166 | - name: Wait other jobs 167 | uses: kachick/wait-other-jobs@v3.8.1 168 | timeout-minutes: 10 169 | with: 170 | skip-same-workflow: 'true' 171 | skip-list: | 172 | [ 173 | { 174 | "workflowFile": "merge-bot-pr.yml" 175 | } 176 | ] 177 | - name: Publish 178 | run: | 179 | gem push 'release-assets/${{ needs.build.outputs.gem_file }}' 180 | - name: Wait for release to propagate 181 | run: | 182 | gem exec rubygems-await 'release-assets/${{ needs.build.outputs.gem_file }}' 183 | -------------------------------------------------------------------------------- /.github/workflows/signature.yml: -------------------------------------------------------------------------------- 1 | name: Signature 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | paths: 7 | - '.github/workflows/signature.yml' 8 | - 'lib/**' 9 | - 'sig/**' 10 | - '**.gemspec' 11 | - 'Gemfile' 12 | - 'Steepfile' 13 | - 'steep_expectations.yml' 14 | - '.yardopts' 15 | - 'Rakefile' 16 | - '.ruby-version' 17 | pull_request: 18 | paths: 19 | - '.github/workflows/signature.yml' 20 | - 'lib/**' 21 | - 'sig/**' 22 | - '**.gemspec' 23 | - 'Gemfile' 24 | - 'Steepfile' 25 | - 'steep_expectations.yml' 26 | - '.yardopts' 27 | - 'Rakefile' 28 | - '.ruby-version' 29 | jobs: 30 | rbs: 31 | timeout-minutes: 15 32 | runs-on: 'ubuntu-24.04' 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 36 | with: 37 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 38 | - run: bundle exec rake rbs 39 | yard: 40 | timeout-minutes: 15 41 | runs-on: 'ubuntu-24.04' 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 45 | with: 46 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 47 | - run: bundle exec rake signature:validate_yard 48 | -------------------------------------------------------------------------------- /.github/workflows/update-nixpkgs-and-versions-in-ci.yml: -------------------------------------------------------------------------------- 1 | name: Update nixpkgs and CI dependencies 2 | on: 3 | pull_request: 4 | paths: 5 | - '.github/workflows/update-nixpkgs-and-versions-in-ci.yml' 6 | schedule: 7 | # Every Monday 10:17 JST 8 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule 9 | - cron: '17 1 * * 1' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | update-nixpkgs: 14 | uses: kachick/selfup/.github/workflows/reusable-bump-flake-lock-and-selfup.yml@v1.2.0 15 | if: (github.event.sender.login == 'kachick') || (github.event_name != 'pull_request') 16 | with: 17 | app_id: ${{ vars.APP_ID }} 18 | dry-run: ${{ github.event_name == 'pull_request' }} 19 | optional-run: | 20 | nix develop --command ruby -e 'puts RUBY_VERSION' > .ruby-version 21 | # https://stackoverflow.com/q/34807971 22 | git update-index -q --really-refresh 23 | git diff-index --quiet HEAD || git commit -m 'Update .ruby-version' .ruby-version 24 | secrets: 25 | APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} 26 | -------------------------------------------------------------------------------- /.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 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | *.bridgesupport 24 | build-iPhoneOS/ 25 | build-iPhoneSimulator/ 26 | 27 | ## Specific to RubyMotion (use of CocoaPods): 28 | # 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 32 | # 33 | # vendor/Pods/ 34 | 35 | ## Documentation cache and generated files: 36 | /.yardoc/ 37 | /_yardoc/ 38 | /doc/ 39 | /rdoc/ 40 | 41 | ## Environment normalization: 42 | /.bundle/* 43 | !.bundle/config 44 | /vendor/bundle 45 | /lib/bundler/man/ 46 | 47 | # For compare with other gems, ref: https://github.com/kachick/ruby-ulid/issues/102 48 | benchmark/compare_with_othergems/*/vendor/ 49 | 50 | # for a library or gem, you might want to ignore these files since the code is 51 | # intended to run in multiple environments; otherwise, check them in: 52 | # Gemfile.lock 53 | # .ruby-version 54 | # .ruby-gemset 55 | 56 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 57 | .rvmrc 58 | 59 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 60 | # .rubocop-https?--* 61 | 62 | *.lock 63 | !flake.lock 64 | .direnv 65 | 66 | .DS_Store 67 | 68 | .tool-versions 69 | 70 | # Unpacked gem directory 71 | /ruby-ulid-*/ 72 | /release-assets/ 73 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-rake 3 | - rubocop-performance 4 | - rubocop-thread_safety 5 | 6 | AllCops: 7 | TargetRubyVersion: 3.3 8 | DisplayCopNames: true 9 | Exclude: 10 | - '**/vendor/**/*' 11 | - 'pkg/**/*' 12 | NewCops: enable 13 | 14 | Metrics: 15 | Enabled: false 16 | 17 | Naming: 18 | Enabled: false 19 | 20 | Layout: 21 | Enabled: true 22 | 23 | Migration: 24 | Enabled: true 25 | 26 | Security: 27 | Enabled: true 28 | 29 | Lint: 30 | Enabled: true 31 | 32 | Style: 33 | Enabled: true 34 | 35 | Bundler: 36 | Enabled: true 37 | 38 | Rake: 39 | Enabled: true 40 | 41 | Rake/Desc: 42 | Enabled: false 43 | 44 | Gemspec: 45 | Enabled: true 46 | 47 | Performance: 48 | Enabled: true 49 | Exclude: 50 | - 'test/**/*.rb' 51 | - '**/*.md' 52 | 53 | ThreadSafety/NewThread: 54 | Enabled: true 55 | Exclude: 56 | - 'test/**/*.rb' 57 | 58 | Security/MarshalLoad: 59 | Enabled: true 60 | Exclude: 61 | - 'test/core/test_ulid_usecase.rb' 62 | 63 | Style/Alias: 64 | Enabled: false 65 | 66 | Style/HashSyntax: 67 | Enabled: true 68 | EnforcedStyle: 'ruby19' 69 | 70 | Style/TrailingMethodEndStatement: 71 | Enabled: true 72 | 73 | Style/MethodDefParentheses: 74 | Enabled: true 75 | EnforcedStyle: 'require_parentheses' 76 | 77 | Style/TrailingCommaInBlockArgs: 78 | Enabled: true 79 | 80 | Style/StringLiterals: 81 | Enabled: true 82 | EnforcedStyle: 'single_quotes' 83 | 84 | Style/StringLiteralsInInterpolation: 85 | Enabled: true 86 | EnforcedStyle: 'single_quotes' 87 | 88 | Style/TopLevelMethodDefinition: 89 | Enabled: true 90 | 91 | Style/AndOr: 92 | Enabled: true 93 | 94 | Style/ArgumentsForwarding: 95 | Enabled: true 96 | 97 | Style/ArrayCoercion: 98 | Enabled: false 99 | 100 | Style/ArrayJoin: 101 | Enabled: true 102 | 103 | Style/AsciiComments: 104 | Enabled: false 105 | 106 | Style/Attr: 107 | Enabled: true 108 | 109 | Style/AutoResourceCleanup: 110 | Enabled: true 111 | 112 | Style/BarePercentLiterals: 113 | Enabled: true 114 | EnforcedStyle: 'percent_q' 115 | 116 | Style/BeginBlock: 117 | Enabled: true 118 | 119 | Style/BisectedAttrAccessor: 120 | Enabled: true 121 | 122 | Style/BlockComments: 123 | Enabled: false 124 | 125 | Style/BlockDelimiters: 126 | Enabled: false 127 | 128 | Style/CaseEquality: 129 | Enabled: false 130 | 131 | Style/CaseLikeIf: 132 | Enabled: true 133 | 134 | Style/CharacterLiteral: 135 | Enabled: false 136 | 137 | Style/ClassAndModuleChildren: 138 | Enabled: true 139 | EnforcedStyle: 'nested' 140 | Exclude: 141 | - 'test/**/*' 142 | 143 | Style/ClassCheck: 144 | Enabled: true 145 | EnforcedStyle: 'kind_of?' 146 | 147 | Style/ClassEqualityComparison: 148 | Enabled: false 149 | 150 | Style/ClassMethods: 151 | Enabled: true 152 | 153 | Style/ClassMethodsDefinitions: 154 | Enabled: false 155 | 156 | Style/ClassVars: 157 | Enabled: true 158 | 159 | Style/CollectionCompact: 160 | Enabled: false 161 | 162 | Style/CollectionMethods: 163 | Enabled: false 164 | 165 | Style/ColonMethodCall: 166 | Enabled: true 167 | 168 | Style/ColonMethodDefinition: 169 | Enabled: true 170 | 171 | Style/CombinableLoops: 172 | Enabled: false 173 | 174 | Style/CommandLiteral: 175 | Enabled: false 176 | 177 | Style/CommentAnnotation: 178 | Enabled: true 179 | 180 | Style/CommentedKeyword: 181 | Enabled: true 182 | 183 | Style/ConditionalAssignment: 184 | Enabled: true 185 | EnforcedStyle: 'assign_to_condition' 186 | 187 | Style/ConstantVisibility: 188 | Enabled: false 189 | 190 | Style/Copyright: 191 | Enabled: false 192 | 193 | Style/DateTime: 194 | Enabled: true 195 | 196 | Style/DefWithParentheses: 197 | Enabled: true 198 | 199 | Style/Dir: 200 | Enabled: true 201 | 202 | Style/DisableCopsWithinSourceCodeDirective: 203 | Enabled: true 204 | Exclude: 205 | - Rakefile 206 | 207 | Style/DocumentDynamicEvalDefinition: 208 | Enabled: false 209 | 210 | Style/Documentation: 211 | Enabled: false 212 | 213 | Style/DocumentationMethod: 214 | Enabled: false 215 | 216 | Style/DoubleCopDisableDirective: 217 | Enabled: false 218 | 219 | Style/DoubleNegation: 220 | Enabled: false 221 | 222 | Style/EachForSimpleLoop: 223 | Enabled: true 224 | 225 | Style/EachWithObject: 226 | Enabled: true 227 | 228 | Style/EmptyBlockParameter: 229 | Enabled: true 230 | 231 | Style/EmptyCaseCondition: 232 | Enabled: false 233 | 234 | Style/EmptyElse: 235 | Enabled: true 236 | EnforcedStyle: 'empty' 237 | 238 | Style/EmptyLambdaParameter: 239 | Enabled: false 240 | 241 | Style/EmptyLiteral: 242 | Enabled: true 243 | 244 | Style/EmptyMethod: 245 | Enabled: true 246 | EnforcedStyle: 'compact' 247 | 248 | Style/Encoding: 249 | Enabled: false 250 | 251 | Style/EndBlock: 252 | Enabled: true 253 | 254 | Style/EndlessMethod: 255 | Enabled: true 256 | EnforcedStyle: 'allow_always' 257 | 258 | Style/EvalWithLocation: 259 | Enabled: true 260 | 261 | Style/EvenOdd: 262 | Enabled: true 263 | 264 | Style/ExpandPathArguments: 265 | Enabled: true 266 | 267 | Style/ExplicitBlockArgument: 268 | Enabled: false 269 | 270 | Style/ExponentialNotation: 271 | Enabled: false 272 | 273 | Style/FloatDivision: 274 | Enabled: true 275 | EnforcedStyle: 'fdiv' 276 | 277 | Style/For: 278 | Enabled: true 279 | EnforcedStyle: 'each' 280 | 281 | Style/FormatString: 282 | Enabled: false 283 | 284 | Style/FormatStringToken: 285 | Enabled: false 286 | 287 | Style/FrozenStringLiteralComment: 288 | Enabled: true 289 | EnforcedStyle: 'always' 290 | 291 | Style/GlobalStdStream: 292 | Enabled: false 293 | 294 | Style/GlobalVars: 295 | Enabled: true 296 | 297 | Style/GuardClause: 298 | Enabled: false 299 | 300 | Style/HashAsLastArrayItem: 301 | Enabled: true 302 | EnforcedStyle: 'braces' 303 | 304 | Style/HashConversion: 305 | Enabled: true 306 | AllowSplatArgument: true 307 | 308 | Style/HashEachMethods: 309 | Enabled: true 310 | 311 | Style/HashExcept: 312 | Enabled: true 313 | 314 | Style/HashLikeCase: 315 | Enabled: false 316 | 317 | Style/HashTransformKeys: 318 | Enabled: true 319 | 320 | Style/HashTransformValues: 321 | Enabled: true 322 | 323 | Style/IdenticalConditionalBranches: 324 | Enabled: false 325 | 326 | Style/IfInsideElse: 327 | Enabled: false 328 | 329 | Style/IfUnlessModifier: 330 | Enabled: false 331 | 332 | Style/IfUnlessModifierOfIfUnless: 333 | Enabled: true 334 | 335 | Style/IfWithBooleanLiteralBranches: 336 | Enabled: true 337 | 338 | Style/IfWithSemicolon: 339 | Enabled: true 340 | 341 | Style/ImplicitRuntimeError: 342 | Enabled: false 343 | 344 | Style/InfiniteLoop: 345 | Enabled: true 346 | 347 | Style/InlineComment: 348 | Enabled: false 349 | 350 | Style/InverseMethods: 351 | Enabled: false 352 | 353 | Style/IpAddresses: 354 | Enabled: false 355 | 356 | Style/KeywordParametersOrder: 357 | Enabled: false 358 | 359 | Style/Lambda: 360 | Enabled: true 361 | EnforcedStyle: 'literal' 362 | 363 | Style/LambdaCall: 364 | Enabled: false 365 | 366 | Style/LineEndConcatenation: 367 | Enabled: false 368 | 369 | Style/MethodCallWithArgsParentheses: 370 | Enabled: true 371 | EnforcedStyle: 'require_parentheses' 372 | IgnoreMacros: 373 | - 'private_class_method' 374 | AllowParenthesesInMultilineCall: true 375 | AllowParenthesesInChaining: true 376 | AllowParenthesesInCamelCaseMethod: true 377 | AllowParenthesesInStringInterpolation: true 378 | Exclude: 379 | - '*.md' 380 | - 'examples/**/*' 381 | 382 | Style/MethodCallWithoutArgsParentheses: 383 | Enabled: true 384 | 385 | Style/MethodCalledOnDoEndBlock: 386 | Enabled: false 387 | 388 | Style/MinMax: 389 | Enabled: false 390 | 391 | Style/MissingElse: 392 | Enabled: false 393 | 394 | Style/MissingRespondToMissing: 395 | Enabled: true 396 | 397 | Style/MixinGrouping: 398 | Enabled: false 399 | 400 | Style/MixinUsage: 401 | Enabled: true 402 | 403 | Style/ModuleFunction: 404 | Enabled: false 405 | 406 | Style/MultilineBlockChain: 407 | Enabled: false 408 | 409 | Style/MultilineIfModifier: 410 | Enabled: false 411 | 412 | Style/MultilineIfThen: 413 | Enabled: true 414 | 415 | Style/MultilineMemoization: 416 | Enabled: false 417 | 418 | Style/MultilineMethodSignature: 419 | Enabled: true 420 | 421 | Style/MultilineTernaryOperator: 422 | Enabled: true 423 | 424 | Style/MultilineWhenThen: 425 | Enabled: true 426 | 427 | Style/MultipleComparison: 428 | Enabled: false 429 | 430 | Style/MutableConstant: 431 | Enabled: true 432 | EnforcedStyle: 'literals' 433 | 434 | Style/NegatedIf: 435 | Enabled: false 436 | 437 | Style/NegatedIfElseCondition: 438 | Enabled: false 439 | 440 | Style/NegatedUnless: 441 | Enabled: true 442 | EnforcedStyle: 'both' 443 | 444 | Style/NegatedWhile: 445 | Enabled: true 446 | 447 | Style/NestedModifier: 448 | Enabled: true 449 | 450 | Style/NestedParenthesizedCalls: 451 | Enabled: true 452 | 453 | Style/NestedTernaryOperator: 454 | Enabled: false 455 | 456 | Style/Next: 457 | Enabled: false 458 | 459 | Style/NilComparison: 460 | Enabled: false 461 | 462 | Style/NilLambda: 463 | Enabled: false 464 | 465 | Style/NonNilCheck: 466 | Enabled: false 467 | 468 | Style/Not: 469 | Enabled: true 470 | 471 | Style/NumericLiteralPrefix: 472 | Enabled: true 473 | EnforcedOctalStyle: 'zero_with_o' 474 | 475 | Style/NumericLiterals: 476 | Enabled: false 477 | 478 | Style/NumericPredicate: 479 | Enabled: false 480 | 481 | Style/OneLineConditional: 482 | Enabled: true 483 | 484 | Style/OptionHash: 485 | Enabled: true 486 | 487 | Style/OptionalArguments: 488 | Enabled: true 489 | 490 | Style/OptionalBooleanParameter: 491 | Enabled: true 492 | 493 | Style/OrAssignment: 494 | Enabled: true 495 | 496 | Style/ParallelAssignment: 497 | Enabled: false 498 | 499 | Style/ParenthesesAroundCondition: 500 | Enabled: false 501 | 502 | Style/PercentLiteralDelimiters: 503 | Enabled: false 504 | 505 | Style/PercentQLiterals: 506 | Enabled: true 507 | EnforcedStyle: 'lower_case_q' 508 | 509 | Style/PerlBackrefs: 510 | Enabled: true 511 | 512 | Style/PreferredHashMethods: 513 | Enabled: true 514 | EnforcedStyle: 'short' 515 | 516 | Style/Proc: 517 | Enabled: false 518 | 519 | Style/RaiseArgs: 520 | Enabled: false 521 | 522 | Style/RandomWithOffset: 523 | Enabled: true 524 | 525 | Style/RedundantArgument: 526 | Enabled: true 527 | 528 | Style/RedundantAssignment: 529 | Enabled: true 530 | 531 | Style/RedundantBegin: 532 | Enabled: false 533 | 534 | Style/RedundantCapitalW: 535 | Enabled: true 536 | 537 | Style/RedundantCondition: 538 | Enabled: true 539 | 540 | Style/RedundantConditional: 541 | Enabled: true 542 | 543 | Style/RedundantException: 544 | Enabled: false 545 | 546 | Style/RedundantFetchBlock: 547 | Enabled: true 548 | 549 | Style/RedundantFileExtensionInRequire: 550 | Enabled: true 551 | 552 | Style/RedundantFreeze: 553 | Enabled: false 554 | 555 | Style/RedundantInterpolation: 556 | Enabled: true 557 | 558 | Style/RedundantParentheses: 559 | Enabled: true 560 | 561 | Style/RedundantPercentQ: 562 | Enabled: false 563 | 564 | Style/RedundantRegexpCharacterClass: 565 | Enabled: true 566 | 567 | Style/RedundantRegexpEscape: 568 | Enabled: true 569 | 570 | Style/RedundantReturn: 571 | Enabled: true 572 | AllowMultipleReturnValues: true 573 | 574 | Style/RedundantSelf: 575 | Enabled: true 576 | 577 | Style/RedundantSelfAssignment: 578 | Enabled: true 579 | 580 | Style/RedundantSort: 581 | Enabled: true 582 | 583 | Style/RedundantSortBy: 584 | Enabled: true 585 | 586 | Style/RegexpLiteral: 587 | Enabled: false 588 | 589 | Style/RescueModifier: 590 | Enabled: true 591 | 592 | Style/RescueStandardError: 593 | Enabled: true 594 | EnforcedStyle: 'implicit' 595 | 596 | Style/ReturnNil: 597 | Enabled: false 598 | 599 | Style/SafeNavigation: 600 | Enabled: false 601 | 602 | Style/Sample: 603 | Enabled: true 604 | 605 | Style/SelfAssignment: 606 | Enabled: false 607 | 608 | Style/Semicolon: 609 | Enabled: true 610 | AllowAsExpressionSeparator: true 611 | 612 | Style/Send: 613 | Enabled: true 614 | 615 | Style/SignalException: 616 | Enabled: false 617 | 618 | Style/SingleArgumentDig: 619 | Enabled: true 620 | 621 | Style/SingleLineBlockParams: 622 | Enabled: false 623 | 624 | Style/SingleLineMethods: 625 | Enabled: false 626 | 627 | Style/SlicingWithRange: 628 | Enabled: true 629 | 630 | Style/SoleNestedConditional: 631 | Enabled: false 632 | 633 | Style/SpecialGlobalVars: 634 | Enabled: false 635 | 636 | Style/StabbyLambdaParentheses: 637 | Enabled: false 638 | 639 | Style/StaticClass: 640 | Enabled: false 641 | 642 | Style/StderrPuts: 643 | Enabled: false 644 | 645 | Style/StringChars: 646 | Enabled: true 647 | 648 | Style/StringConcatenation: 649 | Enabled: false 650 | 651 | Style/StringHashKeys: 652 | Enabled: false 653 | 654 | Style/StringMethods: 655 | Enabled: false 656 | 657 | Style/Strip: 658 | Enabled: true 659 | 660 | Style/DataInheritance: 661 | Enabled: true 662 | Exclude: 663 | # Using `Fields = Data.define do; end` syntax made https://github.com/kachick/ruby-ulid/issues/233 again. So use class syntax instead 664 | - 'lib/ulid/uuid/fields.rb' 665 | 666 | Style/SwapValues: 667 | Enabled: false 668 | 669 | Style/SymbolArray: 670 | Enabled: true 671 | 672 | Style/SymbolLiteral: 673 | Enabled: true 674 | 675 | Style/SymbolProc: 676 | Enabled: true 677 | 678 | Style/TernaryParentheses: 679 | Enabled: true 680 | EnforcedStyle: 'require_parentheses_when_complex' 681 | 682 | Style/TrailingBodyOnClass: 683 | Enabled: true 684 | 685 | Style/TrailingBodyOnMethodDefinition: 686 | Enabled: true 687 | 688 | Style/TrailingBodyOnModule: 689 | Enabled: true 690 | 691 | Style/TrailingCommaInArguments: 692 | Enabled: true 693 | EnforcedStyleForMultiline: 'no_comma' 694 | 695 | Style/TrailingCommaInArrayLiteral: 696 | Enabled: true 697 | EnforcedStyleForMultiline: 'no_comma' 698 | 699 | Style/TrailingCommaInHashLiteral: 700 | Enabled: true 701 | EnforcedStyleForMultiline: 'no_comma' 702 | 703 | Style/TrailingUnderscoreVariable: 704 | Enabled: true 705 | AllowNamedUnderscoreVariables: true 706 | 707 | Style/TrivialAccessors: 708 | Enabled: true 709 | ExactNameMatch: true 710 | AllowPredicates: true 711 | AllowDSLWriters: true 712 | IgnoreClassMethods: false 713 | 714 | Style/UnlessElse: 715 | Enabled: true 716 | 717 | Style/UnlessLogicalOperators: 718 | Enabled: false 719 | 720 | Style/UnpackFirst: 721 | Enabled: true 722 | 723 | Style/VariableInterpolation: 724 | Enabled: false 725 | 726 | Style/WhenThen: 727 | Enabled: true 728 | 729 | Style/WhileUntilDo: 730 | Enabled: true 731 | 732 | Style/WhileUntilModifier: 733 | Enabled: false 734 | 735 | Style/WordArray: 736 | Enabled: false 737 | 738 | Style/YodaCondition: 739 | Enabled: false 740 | 741 | Style/YodaExpression: 742 | Enabled: false 743 | 744 | Style/ZeroLengthPredicate: 745 | Enabled: true 746 | 747 | Style/InPatternThen: 748 | Enabled: true 749 | 750 | Style/QuotedSymbols: 751 | Enabled: true 752 | EnforcedStyle: 'single_quotes' 753 | 754 | Style/MultilineInPatternThen: 755 | Enabled: true 756 | 757 | # https://github.com/rubocop/rubocop/pull/11528 758 | # I disagree that this is called "redundant". Similar to always using single quotes 759 | Style/RedundantHeredocDelimiterQuotes: 760 | Enabled: false 761 | 762 | Layout/TrailingEmptyLines: 763 | Enabled: true 764 | 765 | Layout/TrailingWhitespace: 766 | Enabled: true 767 | 768 | Layout/SpaceBeforeFirstArg: 769 | Enabled: true 770 | 771 | Layout/SpaceBeforeSemicolon: 772 | Enabled: true 773 | 774 | Layout/SpaceInsideArrayPercentLiteral: 775 | Enabled: true 776 | 777 | Layout/SpaceInsideBlockBraces: 778 | Enabled: true 779 | 780 | Layout/SpaceInsideParens: 781 | Enabled: true 782 | 783 | Layout/SpaceInsidePercentLiteralDelimiters: 784 | Enabled: true 785 | 786 | Layout/SpaceInsideRangeLiteral: 787 | Enabled: true 788 | 789 | Layout/SpaceInsideReferenceBrackets: 790 | Enabled: true 791 | 792 | Layout/SpaceInsideStringInterpolation: 793 | Enabled: true 794 | 795 | Layout/SpaceAroundMethodCallOperator: 796 | Enabled: true 797 | 798 | Layout/LineLength: 799 | Enabled: false 800 | 801 | Layout/HeredocIndentation: 802 | Enabled: false 803 | 804 | Layout/SpaceAroundOperators: 805 | Enabled: true 806 | EnforcedStyleForExponentOperator: 'space' 807 | 808 | Layout/MultilineMethodCallBraceLayout: 809 | Enabled: false 810 | 811 | Layout/SpaceInsideHashLiteralBraces: 812 | Enabled: true 813 | 814 | Layout/EmptyLineBetweenDefs: 815 | Enabled: true 816 | AllowAdjacentOneLineDefs: true 817 | 818 | Layout/SpaceAroundEqualsInParameterDefault: 819 | Enabled: true 820 | EnforcedStyle: 'no_space' 821 | 822 | Layout/SpaceAroundKeyword: 823 | Enabled: true 824 | 825 | Layout/ArgumentAlignment: 826 | Enabled: true 827 | 828 | Layout/CaseIndentation: 829 | Enabled: true 830 | 831 | Naming/MethodName: 832 | Enabled: true 833 | EnforcedStyle: 'snake_case' 834 | 835 | Naming/FileName: 836 | Enabled: true 837 | Exclude: 838 | - 'lib/ruby-ulid.rb' 839 | - '**.md' 840 | 841 | Naming/ConstantName: 842 | # Disabling because we can use the constant assignment syntax for class objects 843 | Enabled: false 844 | 845 | Naming/ClassAndModuleCamelCase: 846 | Enabled: true 847 | 848 | Naming/BlockParameterName: 849 | Enabled: true 850 | 851 | Naming/HeredocDelimiterCase: 852 | Enabled: true 853 | 854 | Naming/HeredocDelimiterNaming: 855 | Enabled: true 856 | 857 | Naming/MemoizedInstanceVariableName: 858 | Enabled: false 859 | 860 | Naming/MethodParameterName: 861 | Enabled: false 862 | 863 | Naming/PredicateName: 864 | Enabled: true 865 | 866 | Naming/RescuedExceptionsVariableName: 867 | Enabled: false 868 | 869 | Naming/VariableName: 870 | Enabled: true 871 | EnforcedStyle: 'snake_case' 872 | 873 | Naming/VariableNumber: 874 | Enabled: false 875 | 876 | Bundler/OrderedGems: 877 | Enabled: false 878 | 879 | Gemspec/OrderedDependencies: 880 | Enabled: false 881 | 882 | Lint/AmbiguousAssignment: 883 | Enabled: true 884 | 885 | Lint/AmbiguousBlockAssociation: 886 | Enabled: true 887 | 888 | Lint/AmbiguousOperator: 889 | Enabled: true 890 | 891 | Lint/AmbiguousRegexpLiteral: 892 | Enabled: true 893 | 894 | Lint/InheritException: 895 | Enabled: false 896 | 897 | Lint/RedundantStringCoercion: 898 | Enabled: false 899 | 900 | Lint/StructNewOverride: 901 | Enabled: true 902 | 903 | Lint/BinaryOperatorWithIdenticalOperands: 904 | Exclude: 905 | - 'test/**/*' 906 | 907 | Lint/UnusedMethodArgument: 908 | Enabled: false 909 | 910 | Lint/AssignmentInCondition: 911 | Enabled: false 912 | 913 | Lint/RescueException: 914 | Enabled: false 915 | 916 | Lint/DuplicateMethods: 917 | Enabled: true 918 | 919 | Lint/BigDecimalNew: 920 | Enabled: true 921 | 922 | Lint/BooleanSymbol: 923 | Enabled: false 924 | 925 | Lint/CircularArgumentReference: 926 | Enabled: true 927 | 928 | Lint/ConstantDefinitionInBlock: 929 | Enabled: true 930 | 931 | Lint/ConstantResolution: 932 | Enabled: false 933 | 934 | Lint/EmptyInPattern: 935 | Enabled: true 936 | 937 | # https://docs.rubocop.org/rubocop/cops_lint.html#lintnumberconversion 938 | # Want to use, however currently it does not stable work. Keep in mind. 939 | # e.g. base number is not covered. AllowedPatterns can not be supported even if documented. 940 | Lint/NumberConversion: 941 | Enabled: true 942 | Include: 943 | - 'lib/ulid/crockford_base32.rb' 944 | 945 | Lint/Debugger: 946 | Exclude: 947 | - 'benchmark/**/*.rb' 948 | - 'examples/**/*.rb' 949 | - 'scripts/**/*.rb' 950 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.3 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "github.vscode-github-actions", 5 | "tekumara.typos-vscode", 6 | "soutaro.steep-vscode", 7 | "soutaro.rbs-syntax", 8 | "dprint.dprint", 9 | "rubocop.vscode-rubocop", 10 | "jnoortheen.nix-ide" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "dprint.dprint", 3 | "editor.formatOnSave": true, 4 | "[ruby]": { 5 | "editor.defaultFormatter": "rubocop.vscode-rubocop" 6 | }, 7 | "[nix]": { 8 | "editor.defaultFormatter": "jnoortheen.nix-ide" 9 | }, 10 | "nix.enableLanguageServer": true, 11 | "nix.serverPath": "nixd" 12 | } 13 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --exclude lib/ulid/uuid/fields.rb 2 | --fail-on-warning 3 | --tag dynamic:"To avoid conflict with rbs(steep) annotation. See https://github.com/soutaro/steep/tree/3994d5af99c458c2799331258fbe463db3b26d9a#2-write-ruby-code" 4 | --hide-tag dynamic 5 | - 6 | LICENSE.txt 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | - Reporting bugs 4 | - Suggesting features 5 | - Creating PRs 6 | 7 | Welcome all of the contributions! 8 | 9 | ## Setup 10 | 11 | Needs your ruby, which is a supported version, and some external tools for development.\ 12 | Author is using [Nix](https://nixos.org/), and put the [definition](flake.nix). 13 | 14 | ```console 15 | $ git clone git@github.com:kachick/ruby-ulid.git 16 | $ cd ./ruby-ulid 17 | $ nix develop 18 | $ dprint --version 19 | $ bundle install || bundle update 20 | ``` 21 | 22 | ## Dprint 23 | 24 | Using [dprint](https://dprint.dev/) for common formatter except ruby. 25 | 26 | ```console 27 | $ dprint check 28 | $ dprint fmt 29 | ... 30 | ``` 31 | 32 | ## Rubocop 33 | 34 | Using rubocop as a ruby formatter. 35 | 36 | ```console 37 | $ bundle exec rubocop 38 | $ bundle exec rubocop --autocorrect 39 | ... 40 | ``` 41 | 42 | ## Touch the development version with REPL 43 | 44 | ```console 45 | $ ./bin/console 46 | # Starting up IRB with loading developing ULID library 47 | irb(main):001:0> ULID::VERSION 48 | => "0.9.0" 49 | ``` 50 | 51 | ```ruby 52 | # On IRB, you can touch behaviors even if it is undocumented 53 | ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA) 54 | ls ULID 55 | 56 | # constants: 57 | # ..., Error, ...VERSION, ... 58 | # Module#methods: ... 59 | # Class#methods: ... 60 | # ULID.methods: 61 | # at decode_time encode floor from_integer generate max min 62 | # normalize normalized? parse parse_variant_format range sample scan try_convert 63 | # valid_as_variant_format? 64 | # => nil 65 | ``` 66 | 67 | ## How to make ideal PRs (Not a mandatory rule, feel free to PR!) 68 | 69 | If you try to add/change/fix features, please update and/or confirm core feature's tests are not broken. 70 | 71 | ```console 72 | $ bundle exec rake test 73 | $ echo $? 74 | 0 75 | ``` 76 | 77 | If you want to run partially tests, test-unit can take some patterns(String/Regexp) with the naming. 78 | 79 | ```console 80 | ❯ bundle exec rake test TESTOPTS="-v -n'/test_.*generate/i'" 81 | Loaded suite /nix/store/d2grc9vz9d3bgl3ncjj7s0nrqi4xz003-ruby-3.2.1/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/rake_test_loader 82 | Started 83 | TestULIDClass: 84 | test_generate: .: (0.000568) 85 | test_generate_with_invalid_arguments: .: (0.001486) 86 | TestULIDMonotonicGenerator: 87 | test_generate_and_encode_can_be_used_together: .: (0.001844) 88 | test_generate_ignores_lower_moment_than_last_is_given: .: (0.000249) 89 | test_generate_just_bump_1_when_same_moment: .: (0.000173) 90 | test_generate_optionally_take_moment_as_milliseconds: .: (0.001897) 91 | test_generate_optionally_take_moment_as_time: .: (0.004004) 92 | test_generate_raises_overflow_when_called_on_max_entropy: .: (0.000288) 93 | test_generate_with_negative_moment: .: (0.000098) 94 | 95 | Finished in 0.011706387 seconds. 96 | ------------------------------------------------------------------------------------------------------------------------ 97 | 9 tests, 503 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 98 | 100% passed 99 | ------------------------------------------------------------------------------------------------------------------------ 100 | 768.81 tests/s, 42968.00 assertions/s 101 | ``` 102 | 103 | CI includes other heavy tests, signature check, lint, if you want to check them in own machine, below command is the one. 104 | 105 | But please don't hesitate to send PRs even if something fail in this command! 106 | 107 | ```console 108 | $ bundle exec rake # mostly, lightweight 109 | $ bundle exec rake simulate_ci # all, heavy 110 | $ echo $? 111 | 0 112 | ``` 113 | 114 | If you try to improve any performance issue, please add benchmarking and check the result of before and after. 115 | 116 | ```console 117 | $ bundle exec ruby benchmark/the_added_file.rb 118 | # Showing the results 119 | ``` 120 | 121 | ## ADR - Architecture decision record 122 | 123 | ### What is `ADR`? 124 | 125 | - [English](https://github.com/joelparkerhenderson/architecture_decision_record) 126 | - [Japanese](https://quipper.hatenablog.com/entry/architecture_decision_records) 127 | 128 | ### Adding dependencies for this gem 129 | 130 | - Keep no runtime dependencies 131 | - Might be unavoidably needed latest versions of ruby standard libraries from `https://github.com/ruby/*` 132 | - Keep clean environment in `test` group. Do not add gems like `active_support` into `test` group # ref: [My struggle](https://github.com/kachick/ruby-ulid/pull/42#discussion_r623960639) 133 | 134 | ### Adding `ULID` instance variables 135 | 136 | - Basically should be reduced. ref: [#91](kachick/ruby-ulid#91), [#236](kachick/ruby-ulid#236) 137 | - When having some objects, they should be frozen. ref: [#126](kachick/ruby-ulid#126) 138 | 139 | ## Tasks to drop Ruby 3.3 140 | 141 | - grep `RUBY_VERSION` guards 142 | - grep `3.3` and `3.4` 143 | - Update gemspec and `TargetRubyVersion` in .rubocop.yml 144 | 145 | ## Use profiler 146 | 147 | ```console 148 | > bundle install || bundle update 149 | ❯ bundle exec rake stackprof 150 | rm -rf ./tmp/stackprof-* 151 | bundle exec ruby ./scripts/prof.rb 152 | bundle exec stackprof tmp/stackprof-wall-*.dump --text --limit 5 153 | ================================== 154 | Mode: wall(1000) 155 | Samples: 445 (0.00% miss rate) 156 | GC: 50 (11.24%) 157 | ================================== 158 | TOTAL (pct) SAMPLES (pct) FRAME 159 | 86 (19.3%) 86 (19.3%) String#tr 160 | 88 (19.8%) 40 (9.0%) ULID::Utils.encode_base32hex 161 | 38 (8.5%) 38 (8.5%) (sweeping) 162 | 41 (9.2%) 38 (8.5%) Time#to_r 163 | 80 (18.0%) 34 (7.6%) Random::Formatter#random_number 164 | bundle exec stackprof tmp/stackprof-cpu-*.dump --text --limit 5 165 | ================================== 166 | Mode: cpu(1000) 167 | Samples: 45 (0.00% miss rate) 168 | GC: 6 (13.33%) 169 | ================================== 170 | TOTAL (pct) SAMPLES (pct) FRAME 171 | 8 (17.8%) 8 (17.8%) String#tr 172 | 6 (13.3%) 6 (13.3%) Random.urandom 173 | 5 (11.1%) 5 (11.1%) Time#to_r 174 | 4 (8.9%) 4 (8.9%) Rational#* 175 | 10 (22.2%) 4 (8.9%) Random::Formatter#random_number 176 | ``` 177 | 178 | See [#213](https://github.com/kachick/ruby-ulid/pull/213) for further detail 179 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source('https://rubygems.org') 4 | 5 | gemspec 6 | 7 | group(:development, :test) do 8 | gem('rake', '~> 13.3.0') 9 | gem('irb', '~> 1.15.2') 10 | gem('irb-power_assert', '0.4.0') 11 | gem('perfect_toml', '~> 0.9.0', require: false) 12 | end 13 | 14 | group(:development) do 15 | gem('debug', '~> 1.10.0', require: false) 16 | gem('rbs', '~> 3.6.1', require: false) 17 | gem('steep', '~> 1.8.3', require: false) 18 | gem('benchmark-ips', '~> 2.14.0', require: false) 19 | gem('stackprof') 20 | gem('yard', '~> 0.9.37', require: false) 21 | # Don't relax rubocop family versions with `~> the_version`, rubocop often introduce breaking changes in patch versions. See #722 22 | gem('rubocop', '1.75.8', require: false) 23 | gem('rubocop-rake', '0.7.1', require: false) 24 | gem('rubocop-performance', '1.25.0', require: false) 25 | gem('rubocop-thread_safety', '0.7.2', require: false) 26 | end 27 | 28 | group(:test) do 29 | gem('test-unit', '~> 3.6.8') 30 | gem('warning', '~> 1.5.0') 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kenichi Kamiya 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 | # ruby-ulid 2 | 3 | [![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml?query=branch%3Amain) 4 | [![Gem Version](https://badge.fury.io/rb/ruby-ulid.svg)](http://badge.fury.io/rb/ruby-ulid) 5 | 6 | This gem is in maintenance mode, I have no plan to add new features.\ 7 | The reason is UUID v7 has been accepted in [IETF](https://www.rfc-editor.org/rfc/rfc9562.html) and [ruby's securerandom](https://github.com/ruby/securerandom/pull/19). See [UUID section](#uuid) for detail. 8 | 9 | ## Overview 10 | 11 | [ulid/spec](https://github.com/ulid/spec) defines some useful features.\ 12 | In particular, it has uniqueness, randomness, extractable timestamps, and sortability.\ 13 | This gem aims to provide the generator, optional monotonicity, parser, and other manipulations around ULID.\ 14 | [RBS](https://github.com/ruby/rbs) definitions are also included. 15 | 16 | --- 17 | 18 | ![ULIDlogo](./assets/logo.png) 19 | 20 | ## Universally Unique Lexicographically Sortable Identifier 21 | 22 | UUID can be suboptimal for many uses-cases because: 23 | 24 | - It isn't the most character efficient way of encoding 128 bits of randomness 25 | - UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address 26 | - UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures 27 | - UUID v4 provides no other information than randomness which can cause fragmentation in many data structures 28 | 29 | Instead, herein is proposed ULID: 30 | 31 | - 128-bit compatibility with UUID 32 | - 1.21e+24 unique ULIDs per millisecond 33 | - Lexicographically sortable! 34 | - Canonically encoded as a 26 character string, as opposed to the 36 character UUID 35 | - Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character) 36 | - Case insensitive 37 | - No special characters (URL safe) 38 | - Monotonic sort order (correctly detects and handles the same millisecond) 39 | 40 | ## Usage 41 | 42 | ### Install 43 | 44 | Tested only in the last 2 Rubies. So you need Ruby 3.3 or higher. 45 | 46 | Add this line to your `Gemfile`. 47 | 48 | ```ruby 49 | gem('ruby-ulid', '~> 0.9.0') 50 | ``` 51 | 52 | And load it. 53 | 54 | ```ruby 55 | require 'ulid' 56 | ``` 57 | 58 | NOTE: This README contains information about the development version.\ 59 | If you would like to see released version's one. [Look at the ref](https://github.com/kachick/ruby-ulid/tree/v0.9.0). 60 | 61 | In [Nix](https://nixos.org/), you can skip the installation steps for both ruby and ruby-ulid to try. 62 | 63 | ```console 64 | > nix run github:kachick/ruby-ulid#ruby -- -e 'p ULID.generate' 65 | ULID(2024-03-03 18:37:06.152 UTC: 01HR2SNY789ZZ027EDJEHAGQ62) 66 | 67 | > nix run github:kachick/ruby-ulid#irb 68 | irb(main):001:0> ULID.parse('01H66XG2A9WWYRCYGPA62T4AZA') 69 | => ULID(2023-07-25 16:18:12.937 UTC: 01H66XG2A9WWYRCYGPA62T4AZA) 70 | ``` 71 | 72 | ### Generator and Parser 73 | 74 | `ULID.generate` returns `ULID` instance. It is not just a string. 75 | 76 | ```ruby 77 | ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA) 78 | ``` 79 | 80 | `ULID.parse` returns `ULID` instance from exists encoded ULIDs. 81 | 82 | ```ruby 83 | ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA) 84 | ``` 85 | 86 | It has inspector methods. 87 | 88 | ```ruby 89 | ulid.to_time #=> 2021-04-27 17:27:22.826 UTC 90 | ulid.milliseconds #=> 1619544442826 91 | ulid.encode #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA" 92 | ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA" 93 | ulid.timestamp #=> "01F4A5Y1YA" 94 | ulid.randomness #=> "QCYAYCTC7GRMJ9AA" 95 | ulid.to_i #=> 1957909092946624190749577070267409738 96 | ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74] 97 | ``` 98 | 99 | `ULID.generate` can take fixed `Time` instance. `ULID.at` is the shorthand. 100 | 101 | ```ruby 102 | time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC 103 | ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P) 104 | ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP) 105 | ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6) 106 | ``` 107 | 108 | Also `ULID.encode` and `ULID.decode_time` can be used to get primitive values for most usecases. 109 | 110 | `ULID.encode` returns [normalized](#variants-of-format) String without ULID object creation.\ 111 | It can take same arguments as `ULID.generate`. 112 | 113 | ```ruby 114 | ULID.encode #=> "01G86M42Q6SJ9XQM2ZRM6JRDSF" 115 | ULID.encode(moment: Time.at(946684800).utc) #=> "00VHNCZB00SYG7RCEXZC9DA4E1" 116 | ``` 117 | 118 | `ULID.decode_time` returns Time. It can take `in` keyarg as same as `Time.at`. 119 | 120 | ```ruby 121 | ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1') #=> 2000-01-01 00:00:00 UTC 122 | ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1', in: '+09:00') #=> 2000-01-01 09:00:00 +0900 123 | ``` 124 | 125 | This project does not prioritize on the speed. However it actually works faster than others! :zap: 126 | 127 | Snapshot on v0.8.0 with Ruby 3.2.1 is below 128 | 129 | - Generator is 1.9x faster than - [ulid gem - v1.4.0](https://github.com/rafaelsales/ulid) 130 | - Generator is 2.0x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby) 131 | - Parser is 3.1x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby) 132 | 133 | You can see further detail at [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark). 134 | 135 | ### Sortable by timestamp 136 | 137 | ULIDs are sortable when they are generated in different timestamp with milliseconds precision. 138 | 139 | ```ruby 140 | ulids = 1000.times.map do 141 | sleep(0.001) 142 | ULID.generate 143 | end 144 | ulids.uniq(&:to_time).size #=> 1000 145 | ulids.sort == ulids #=> true 146 | ``` 147 | 148 | The basic generator prefers `randomness`, the results in the same milliseconds are not sortable. 149 | 150 | ```ruby 151 | ulids = 10000.times.map do 152 | ULID.generate 153 | end 154 | ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in environment) 155 | ulids.sort == ulids #=> false 156 | ``` 157 | 158 | ### How to keep `Sortable` even if in same timestamp 159 | 160 | If you prefer `sortability`, you can use `MonotonicGenerator` instead.\ 161 | It is referred to as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) in the spec.\ 162 | (Although it starts with a new random value when the timestamp is changed) 163 | 164 | ```ruby 165 | monotonic_generator = ULID::MonotonicGenerator.new 166 | ulids = 10000.times.map do 167 | monotonic_generator.generate 168 | end 169 | sample_ulids_by_the_time = ulids.uniq(&:to_time) 170 | sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment) 171 | 172 | # In same milliseconds creation, it just increments the end of randomness part 173 | ulids.take(3) #=> 174 | # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4), 175 | # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5), 176 | # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6)] 177 | 178 | # When the milliseconds is updated, it starts with new randomness 179 | sample_ulids_by_the_time.take(3) #=> 180 | # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4), 181 | # ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204), 182 | # ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K)] 183 | 184 | ulids.sort == ulids #=> true 185 | ``` 186 | 187 | Same instance of `ULID::MonotonicGenerator` does not generate duplicated ULIDs even in multi threads environment. It is implemented with [Monitor](https://bugs.ruby-lang.org/issues/16255). 188 | 189 | ### Filtering IDs with `Time` 190 | 191 | `ULID` can be element of the `Range`. If they were generated with monotonic generator, ID based filtering is easy and reliable. 192 | 193 | ```ruby 194 | include_end = ulid1..ulid2 195 | exclude_end = ulid1...ulid2 196 | 197 | ulids.grep(one_of_the_above) 198 | ulids.grep_v(one_of_the_above) 199 | ``` 200 | 201 | When want to filter ULIDs with `Time`, we should consider to handle the precision.\ 202 | So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]` 203 | 204 | ```ruby 205 | # Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1 206 | include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2 207 | exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2 208 | 209 | # Below patterns are acceptable 210 | pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1` 211 | until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` 212 | until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit 213 | 214 | # So you can use the generated range objects as below 215 | ulids.grep(one_of_the_above) 216 | ulids.grep_v(one_of_the_above) 217 | #=> I hope the results should be actually you want! 218 | ``` 219 | 220 | If you want to manually handle the Time objects, `ULID.floor` returns new `Time` with truncating excess precisions in ULID spec. 221 | 222 | ```ruby 223 | time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC 224 | ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC 225 | ``` 226 | 227 | ### Tools 228 | 229 | #### Scanner for string (e.g. `JSON`) 230 | 231 | For rough operations, `ULID.scan` might be useful. 232 | 233 | ```ruby 234 | json = <<'JSON' 235 | { 236 | "id": "01F4GNAV5ZR6FJQ5SFQC7WDSY3", 237 | "author": { 238 | "id": "01F4GNBXW1AM2KWW52PVT3ZY9X", 239 | "name": "kachick" 240 | }, 241 | "title": "My awesome blog post", 242 | "comments": [ 243 | { 244 | "id": "01F4GNCNC3CH0BCRZBPPDEKBKS", 245 | "commenter": { 246 | "id": "01F4GNBXW1AM2KWW52PVT3ZY9X", 247 | "name": "kachick" 248 | } 249 | }, 250 | { 251 | "id": "01F4GNCXAMXQ1SGBH5XCR6ZH0M", 252 | "commenter": { 253 | "id": "01F4GND4RYYSKNAADHQ9BNXAWJ", 254 | "name": "pankona" 255 | } 256 | } 257 | ] 258 | } 259 | JSON 260 | 261 | ULID.scan(json).to_a 262 | #=> 263 | # [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3), 264 | # ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X), 265 | # ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS), 266 | # ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X), 267 | # ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M), 268 | # ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)] 269 | ``` 270 | 271 | #### Get boundary ULIDs 272 | 273 | `ULID.min` and `ULID.max` return termination values for ULID spec. 274 | 275 | It can take `Time` instance as an optional argument. Then returns min/max ID that has limit of randomness part in the time. 276 | 277 | ```ruby 278 | ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000) 279 | ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ) 280 | 281 | time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC 282 | ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000) 283 | ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ) 284 | ``` 285 | 286 | #### As an element in Enumerable and Range 287 | 288 | `ULID#next` and `ULID#succ` returns next(successor) ULID.\ 289 | Especially `ULID#succ` makes it possible `Range[ULID]#each`. 290 | 291 | NOTE: However basically `Range[ULID]#each` should not be used. Incrementing 128 bits IDs are not reasonable operation in most cases. 292 | 293 | ```ruby 294 | ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ" 295 | ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM0000000000000000" 296 | ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil 297 | ``` 298 | 299 | `ULID#pred` returns predecessor ULID. 300 | 301 | ```ruby 302 | ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000" 303 | ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZZZ" 304 | ULID.parse('00000000000000000000000000').pred #=> nil 305 | ``` 306 | 307 | `ULID#+` is also provided to realize `Range#step` since [ruby-3.4.0 spec changes](https://bugs.ruby-lang.org/issues/18368). 308 | 309 | ```ruby 310 | # This code works only in ruby-3.4.0dev or later 311 | (ULID.min...).step(42).take(3) 312 | # => 313 | [ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000), 314 | ULID(1970-01-01 00:00:00.000 UTC: 0000000000000000000000001A), 315 | ULID(1970-01-01 00:00:00.000 UTC: 0000000000000000000000002M)] 316 | ``` 317 | 318 | #### Test helpers 319 | 320 | `ULID.sample` returns random ULIDs. 321 | 322 | Basically ignores generating time. 323 | 324 | ```ruby 325 | ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST) 326 | ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5) 327 | ULID.sample(0) #=> [] 328 | ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)] 329 | ULID.sample(3) 330 | #=> 331 | #[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT), 332 | # ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2), 333 | # ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0)] 334 | ``` 335 | 336 | You can specify a range object for the timestamp restriction, see also `ULID.range`. 337 | 338 | ```ruby 339 | ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA) 340 | ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4) 341 | ulids = ULID.sample(3, period: ulid1..ulid2) 342 | #=> 343 | #[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X), 344 | # ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ), 345 | # ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW)] 346 | ULID.sample(3, period: ulid1.to_time..ulid2.to_time) 347 | #=> 348 | # [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW), 349 | # ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2), 350 | # ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW)] 351 | ``` 352 | 353 | #### Variants of format 354 | 355 | I'm afraid so, we should consider [Current ULID spec](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#universally-unique-lexicographically-sortable-identifier) has `orthographical variants of the format` possibilities. 356 | 357 | > Case insensitive 358 | 359 | I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase.\ 360 | However it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3). 361 | 362 | > Uses Crockford's base32 for better efficiency and readability (5 bits per character) 363 | 364 | The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`.\ 365 | And accepts freestyle inserting `Hyphens (-)`.\ 366 | To consider this patterns or not is different in each implementations. 367 | 368 | I have suggested to clarify `subset of Crockford's base32` in [ulid/spec#57](https://github.com/ulid/spec/pull/57). 369 | 370 | This gem provides some methods to handle the nasty possibilities. 371 | 372 | `ULID.normalize`, `ULID.normalized?`, `ULID.valid_as_variant_format?` and `ULID.parse_variant_format` 373 | 374 | ```ruby 375 | ULID.normalize('01g70y0y7g-z1xwdarexergsddd') #=> "01G70Y0Y7GZ1XWDAREXERGSDDD" 376 | ULID.normalized?('01g70y0y7g-z1xwdarexergsddd') #=> false 377 | ULID.normalized?('01G70Y0Y7GZ1XWDAREXERGSDDD') #=> true 378 | ULID.valid_as_variant_format?('01g70y0y7g-z1xwdarexergsddd') #=> true 379 | ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D) 380 | ``` 381 | 382 | #### UUID 383 | 384 | Both ULID and UUID are 128-bit IDs. But with different specs. Especially, UUID has some versions, for example, UUIDv4 and UUIDv7. 385 | 386 | All UUIDs can be converted to ULID, but only [new versions](https://datatracker.ietf.org/doc/rfc9562/) have a correct "timestamp".\ 387 | Most ULIDs cannot be converted to UUID while maintaining reversibility, because UUID requires version and variants in the fields. 388 | 389 | See also [ulid/spec#64](https://github.com/ulid/spec/issues/64) for further detail. 390 | 391 | For now, this gem provides some methods for UUIDs. 392 | 393 | - Reversibility is preferred: `ULID.from_uuidish`, `ULID.to_uuidish` 394 | - Prefer variants specification: `ULID.from_uuid_v4`, `ULID.from_uuid_v7`, `ULID.to_uuid_v4`, `ULID.to_uuid_v7` 395 | 396 | ```ruby 397 | # All UUIDv4 and UUIDv7 IDs can be reversible even if converted to ULID 398 | uuid_v4 = SecureRandom.uuid_v4 399 | ULID.from_uuidish(uuid_v4) == ULID.from_uuid_v4(uuid_v4) #=> true 400 | ULID.from_uuidish(uuid_v4).to_uuidish == ULID.from_uuid_v4(uuid_v4).to_uuid_v4 #=> true 401 | 402 | # v4 does not have timestamp, v7 has it. 403 | 404 | ULID.from_uuid_v4(SecureRandom.uuid_v4).to_time 405 | # 'f80b3f53-043a-4298-a674-cd83a7fd5d22' => 10612-05-19 16:58:53.882 UTC 406 | 407 | ULID.from_uuid_v7(SecureRandom.uuid_v7).to_time 408 | # '01946f9e-bf58-7be3-8fd4-4606606b05aa' => 2025-01-16 14:57:42.232 UTC 409 | # ULID is officially defined milliseconds precision for the spec. So omit the nanoseconds precisions even if the UUID v7 ID was generated with extra_timestamp_bits >= 1. 410 | 411 | # However most ULIDs cannot be converted to versioned UUID 412 | ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') 413 | ulid.to_uuid_v4 #=> ULID::IrreversibleUUIDError 414 | # So 2 ways to get substitute strings that might satisfy the use case 415 | ulid.to_uuid_v4(force: true) #=> "0179145f-07ca-4b3c-af33-4c3c3149254a" this cannot be reverse to source ULID 416 | ulid == ULID.from_uuid_v4(ulid.to_uuid_v4(force: true)) #=> false 417 | ulid.to_uuidish #=> "0179145f-07ca-bb3c-af33-4c3c3149254a" does not satisfy UUIDv4 spec 418 | ulid == ULID.from_uuidish(ulid.to_uuidish) #=> true 419 | 420 | # Seeing boundary IDs makes it easier to understand 421 | ULID.min.to_uuidish #=> "00000000-0000-0000-0000-000000000000" 422 | ULID.min.to_uuid_v4(force: true) #=> "00000000-0000-4000-8000-000000000000" 423 | ULID.max.to_uuidish #=> "ffffffff-ffff-ffff-ffff-ffffffffffff" 424 | ULID.max.to_uuid_v4(force: true) #=> "ffffffff-ffff-4fff-bfff-ffffffffffff" 425 | ``` 426 | 427 | ## Migration from other gems 428 | 429 | See [wiki page for gem migration](https://github.com/kachick/ruby-ulid/wiki/Gem-migration). 430 | 431 | ## RBS 432 | 433 | - Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox). 434 | - See the overview in [our wiki page for RBS](https://github.com/kachick/ruby-ulid/wiki/RBS) 435 | 436 | ## References 437 | 438 | - [Repository](https://github.com/kachick/ruby-ulid) 439 | - [API documents](https://kachick.github.io/ruby-ulid/) 440 | - [ulid/spec](https://github.com/ulid/spec) 441 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require('bundler/gem_tasks') 4 | 5 | require('rake/testtask') 6 | 7 | begin 8 | require('rubocop/rake_task') 9 | rescue LoadError 10 | puts('can not use rubocop in this environment') 11 | else 12 | RuboCop::RakeTask.new 13 | end 14 | 15 | multitask(default: %i[test signature_all rubocop validate_gem dprint]) 16 | 17 | desc('Keep light weight!') 18 | task(test: :test_core) 19 | 20 | desc('Contains heavy tests. So basically checked in CI only') 21 | task(test_all: %i[test_core test_many_data test_concurrency test_longtime]) 22 | 23 | Rake::TestTask.new(:test_core) do |tt| 24 | tt.pattern = 'test/core/**/test_*.rb' 25 | tt.warning = true 26 | end 27 | 28 | Rake::TestTask.new(:test_many_data) do |tt| 29 | tt.pattern = 'test/many_data/**/test_*.rb' 30 | tt.warning = true 31 | end 32 | 33 | Rake::TestTask.new(:test_concurrency) do |tt| 34 | tt.pattern = 'test/concurrency/**/test_*.rb' 35 | tt.warning = true 36 | end 37 | 38 | Rake::TestTask.new(:test_longtime) do |tt| 39 | tt.pattern = 'test/longtime/**/test_*.rb' 40 | tt.warning = true 41 | end 42 | 43 | desc('Signature check, it means `rbs` and `YARD` syntax correctness') 44 | multitask(rbs: %i[signature:validate_rbs signature:check_rbs_false_positive]) 45 | multitask(signature_all: %i[signature:validate_yard rbs]) 46 | 47 | desc('Simulate CI results in local machine as possible') 48 | multitask(simulate_ci: %i[test_all signature_all rubocop dprint]) 49 | 50 | namespace(:signature) do 51 | desc('Validate `rbs` syntax, this should be passed') 52 | task(:validate_rbs) do 53 | sh('bundle exec rbs -rsecurerandom -rmonitor -I sig validate') 54 | end 55 | 56 | desc('Check `rbs` definition with `steep` and save alerts into ignoring list :<') 57 | task(:save_rbs_errors) do 58 | sh('bundle exec steep check --severity-level=warning --log-level=fatal --save-expectations') 59 | end 60 | 61 | desc('Check `rbs` definition with `steep`, should be passed at least considering steep_expectations.yml') 62 | task(:check_rbs_false_positive) do 63 | sh('bundle exec steep check --severity-level=warning --log-level=fatal --with-expectations') 64 | end 65 | 66 | desc('Run YARD without docs generating for the syntax check') 67 | task(:validate_yard) do 68 | sh('bundle exec yard stats') 69 | end 70 | end 71 | 72 | task(:list_todo) do 73 | sh("bundle exec yard list --query '@todo'") 74 | sh('git', 'grep', '-Pni', 'FIX ?ME', '**/*.rb', '**/*.gemspec', '**/Gemfile') 75 | end 76 | 77 | FileList['benchmark/*.rb'].each do |path| 78 | desc("Rough benchmark for #{File.basename(path)}") 79 | task(path) do 80 | ruby(path) 81 | end 82 | end 83 | 84 | # This can't be used `bundle exec rake`. Use `rake` instead 85 | desc(%q{Compare generating String performance with other gems}) 86 | task(:benchmark_with_other_gems) do 87 | [{ kachick: 'ruby-ulid(This one)' }, { rafaelsales: 'ulid' }, { abachman: 'ulid-ruby' }].each do |gem_name_by_author| 88 | gem_name_by_author.each_pair do |author, gem_name| 89 | puts('-' * 72) 90 | puts("#### #{author} - #{gem_name}") 91 | cd("./benchmark/compare_with_othergems/#{author}") do 92 | sh('bundle install --quiet') 93 | sh('bundle exec ruby -v ./generate.rb') 94 | sh('bundle exec ruby -v ./parser.rb') 95 | end 96 | end 97 | end 98 | end 99 | 100 | task(:stackprof) do 101 | # Cannot use remove_entry_secure for using glob 102 | sh('rm -rf ./tmp/stackprof-*') 103 | sh('bundle exec ruby ./scripts/prof.rb') 104 | sh('bundle exec stackprof tmp/stackprof-wall-*.dump --text --limit 5') 105 | sh('bundle exec stackprof tmp/stackprof-cpu-*.dump --text --limit 5') 106 | end 107 | 108 | desc('Generate samples for snapshot tests') 109 | task(:generate_snapshots) do 110 | ruby('./scripts/generate_snapshots.rb') 111 | end 112 | 113 | task(:validate_gem) do 114 | sh('gem build --strict --norc --backtrace ruby-ulid.gemspec') 115 | end 116 | 117 | desc('To prevent #69 anymore!') 118 | task(:view_packaging_files) do 119 | remove_entry_secure('./pkg') 120 | sh('rake build') 121 | cd('pkg') do 122 | sh('gem unpack *.gem') 123 | sh('tree -I *\.gem') 124 | end 125 | remove_entry_secure('./pkg') 126 | end 127 | 128 | task(:dprint) do 129 | sh('dprint check') 130 | end 131 | 132 | task(:update) do 133 | sh('dprint config update') 134 | end 135 | 136 | desc 'Print dependencies' 137 | task :deps do 138 | sh('ruby --version') 139 | sh('dprint --version') 140 | sh('tree --version') 141 | sh('typos --version') 142 | end 143 | 144 | desc 'Tests except ruby' 145 | task :check_non_ruby do 146 | Rake::Task['dprint'].invoke 147 | sh('typos . .github .vscode') 148 | # nix fmt doesn't have check: https://github.com/NixOS/nix/issues/6918 149 | sh("git ls-files '*.nix' | xargs nixfmt --check") 150 | end 151 | 152 | task :tag do 153 | sh(%q!ruby -r './lib/ulid/version' -e 'puts "v#{ULID::VERSION}"' | xargs --no-run-if-empty --max-lines=1 git tag!) # rubocop:disable Lint/InterpolationCheck 154 | end 155 | -------------------------------------------------------------------------------- /Steepfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | target(:lib) do 4 | signature('sig') 5 | 6 | check('lib') 7 | 8 | library('securerandom', 'monitor') 9 | 10 | configure_code_diagnostics( 11 | Steep::Diagnostic::Ruby::ElseOnExhaustiveCase => :information, 12 | Steep::Diagnostic::Ruby::UnsupportedSyntax => :information 13 | ) 14 | end 15 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["test/many_data/fixtures"] 3 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kachick/ruby-ulid/13c3bcf1825fb2d7d2c9e936c96b88062876c654/assets/logo.png -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/abachman/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source('https://rubygems.org') 4 | 5 | gem('ulid-ruby', '1.0.2') 6 | gem('benchmark-ips', '~> 2.14.0') 7 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/abachman/generate.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require('ulid') 6 | 7 | raise("Bug to setup: #{ULID.methods(false)}") unless ULID.const_defined?(:Identifier) 8 | 9 | Benchmark.ips do |x| 10 | x.report('ULID.generate') do 11 | ULID.generate 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/abachman/parser.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require('ulid') 6 | 7 | raise("Bug to setup: #{ULID.methods(false)}") unless ULID.const_defined?(:Identifier) 8 | 9 | example_encoded_string = '01F4A5Y1YAQCYAYCTC7GRMJ9AA' 10 | 11 | Benchmark.ips do |x| 12 | x.report('ULID.time') do 13 | ULID.time(example_encoded_string) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/kachick/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source('https://rubygems.org') 4 | 5 | gem('ruby-ulid', path: '../../../') 6 | gem('benchmark-ips', '~> 2.14.0') 7 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/kachick/generate.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require('ulid') 6 | 7 | raise("Bug to setup: #{ULID.methods(false)}") unless ULID.const_defined?(:MonotonicGenerator) 8 | 9 | Benchmark.ips do |x| 10 | x.report('ULID.encode') do 11 | ULID.encode 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/kachick/parser.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require('ulid') 6 | 7 | raise("Bug to setup: #{ULID.methods(false)}") unless ULID.const_defined?(:MonotonicGenerator) 8 | 9 | example_encoded_string = '01F4A5Y1YAQCYAYCTC7GRMJ9AA' 10 | 11 | Benchmark.ips do |x| 12 | x.report('ULID.decode_time') do 13 | ULID.decode_time(example_encoded_string) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/rafaelsales/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source('https://rubygems.org') 4 | 5 | gem('ulid', '1.4.0') 6 | gem('benchmark-ips', '~> 2.14.0') 7 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/rafaelsales/generate.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require('ulid') 6 | 7 | raise("Bug to setup: #{ULID.methods(false)}") unless ULID.const_defined?(:Generator) 8 | 9 | Benchmark.ips do |x| 10 | x.report('ULID.generate') do 11 | ULID.generate 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /benchmark/compare_with_othergems/rafaelsales/parser.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require('ulid') 6 | 7 | raise("Bug to setup: #{ULID.methods(false)}") unless ULID.const_defined?(:Generator) 8 | 9 | p("`ulid gem - #{ULID::VERSION}` does not have parsers yet") 10 | -------------------------------------------------------------------------------- /benchmark/core_instance_methods.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | Benchmark.ips do |x| 8 | x.report('ULID#to_s / This is depending to #to_i') { ULID.sample.to_s } 9 | x.report('ULID#to_i') { ULID.sample.to_i } 10 | x.report('ULID#to_time') { ULID.sample.to_time } 11 | x.report('ULID#milliseconds / Should be fast, because nothing any calculations for now') { ULID.sample.milliseconds } 12 | 13 | x.compare! 14 | end 15 | -------------------------------------------------------------------------------- /benchmark/generate_vs_encode.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | Benchmark.ips do |x| 8 | x.report('ULID.encode') { ULID.encode } 9 | x.report('ULID.generate.to_s') { ULID.generate.to_s } 10 | x.report('ULID.generate') { ULID.generate } 11 | x.report('ULID.parse(ULID.encode)') { ULID.parse(ULID.encode) } 12 | x.compare! 13 | end 14 | -------------------------------------------------------------------------------- /benchmark/monotonic_generators_with_different_time.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | monotonic_generator1 = ULID::MonotonicGenerator.new 8 | monotonic_generator2 = ULID::MonotonicGenerator.new 9 | 10 | msec = ULID.generate.milliseconds 11 | 12 | Benchmark.ips do |x| 13 | x.report('ULID.encode with different time') { ULID.encode(moment: msec += 1) } # To compare with no Monotonicity 14 | x.report('ULID::MonotonicGenerator#encode with different time') { monotonic_generator1.encode(moment: msec += 1) } 15 | x.report('ULID::MonotonicGenerator#generate.to_s with different time') { monotonic_generator2.generate(moment: msec += 1).to_s } 16 | x.compare! 17 | end 18 | -------------------------------------------------------------------------------- /benchmark/monotonic_generators_with_now.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | monotonic_generator1 = ULID::MonotonicGenerator.new 8 | monotonic_generator2 = ULID::MonotonicGenerator.new 9 | 10 | Benchmark.ips do |x| 11 | x.report('ULID.encode') { ULID.encode } # # To compare with no Monotonicity 12 | x.report('ULID::MonotonicGenerator#encode') { monotonic_generator1.encode } 13 | x.report('ULID::MonotonicGenerator#generate.to_s') { monotonic_generator2.generate.to_s } 14 | x.compare! 15 | end 16 | -------------------------------------------------------------------------------- /benchmark/parsers.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | fixed_integer = SecureRandom.random_number(ULID::MAX_INTEGER) 8 | sample = ULID.sample 9 | encoded = sample.to_s 10 | 11 | Benchmark.ips do |x| 12 | x.report('ULID.parse') { ULID.parse(encoded) } 13 | x.report('ULID.decode_time') { ULID.decode_time(encoded) } 14 | x.report('ULID.from_integer') { ULID.from_integer(fixed_integer) } 15 | x.compare! 16 | end 17 | -------------------------------------------------------------------------------- /benchmark/sample.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | sample = ULID.sample 8 | moment = sample.to_time 9 | period = moment..(moment + 10000000) 10 | 11 | Benchmark.ips do |x| 12 | x.report('ULID.sample with big number') { ULID.sample(100000) } 13 | x.report('ULID.sample with big number and period(Range[Time])') { ULID.sample(100000, period:) } 14 | x.compare! 15 | end 16 | -------------------------------------------------------------------------------- /benchmark/sort.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | frozen_ulid_objects = ULID.sample(10000).map(&:freeze) 8 | ulid_strings = frozen_ulid_objects.map(&:to_s) 9 | non_cached_ulid_objects = ulid_strings.map { |s| ULID.parse(s) } 10 | 11 | Benchmark.ips do |x| 12 | x.report('Sort ULID instance / After second sorting, cache will be work. So this benchmark is not accurate.') do 13 | non_cached_ulid_objects.shuffle.sort 14 | end 15 | 16 | x.report('Sort frozen ULID instance(some values cached)') do 17 | frozen_ulid_objects.shuffle.sort 18 | end 19 | 20 | x.report('Sort by String encoded ULIDs') do 21 | ulid_strings.shuffle.sort 22 | end 23 | 24 | x.compare! 25 | end 26 | 27 | # I'll check to be sure sorting result is correct. 28 | 29 | unless ulid_strings == non_cached_ulid_objects.map(&:to_s) 30 | raise('Crucial Bug exists!') 31 | end 32 | 33 | unless ulid_strings == frozen_ulid_objects.map(&:to_s) 34 | raise('Crucial Bug exists!') 35 | end 36 | -------------------------------------------------------------------------------- /benchmark/tools.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require('benchmark/ips') 5 | require_relative('../lib/ulid') 6 | 7 | sample = ULID.sample 8 | moment = sample.to_time 9 | period = moment..(moment + 10000000) 10 | 11 | Benchmark.ips do |x| 12 | x.report('ULID.min with no arguments is optimized') { ULID.min } 13 | x.report('ULID.min with moment(Time)') { ULID.min(moment) } 14 | x.report('ULID.max with no arguments is optimized') { ULID.max } 15 | x.report('ULID.max with moment(Time)') { ULID.max(moment) } 16 | x.report('ULID.sample with no arguments') { ULID.sample } 17 | x.report('ULID.sample with period') { ULID.sample(period:) } 18 | x.compare! 19 | end 20 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require('bundler/setup') 5 | require('irb') 6 | require('irb/completion') # easy tab completion ref: https://docs.ruby-lang.org/ja/latest/library/irb=2fcompletion.html 7 | require('irb/power_assert') 8 | 9 | require_relative('../lib/ulid') 10 | 11 | IRB.start 12 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "json": { 3 | }, 4 | "markdown": { 5 | }, 6 | "yaml": { 7 | "quotes": "preferSingle", 8 | "trimTrailingWhitespaces": false 9 | }, 10 | "excludes": [ 11 | ".git", 12 | "**/*lock.json", 13 | "docs", 14 | "doc", 15 | "vendor", 16 | "test/**/fixtures/**" 17 | ], 18 | "plugins": [ 19 | "https://plugins.dprint.dev/json-0.20.0.wasm", 20 | "https://plugins.dprint.dev/markdown-0.18.0.wasm", 21 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/rbs_sandbox/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "soutaro.steep-vscode" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /examples/rbs_sandbox/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source('https://rubygems.org') 4 | 5 | gem('ruby-ulid') 6 | 7 | group(:development) do 8 | gem('rbs', require: false) 9 | gem('steep', require: false) 10 | end 11 | -------------------------------------------------------------------------------- /examples/rbs_sandbox/Steepfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | target(:app) do 4 | signature('sig') 5 | 6 | check('lib') 7 | 8 | library('ruby-ulid') 9 | end 10 | -------------------------------------------------------------------------------- /examples/rbs_sandbox/lib/my_sandbox.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require('ulid') 4 | 5 | ulid = ULID.generate 6 | p ulid.to_time 7 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1748437600, 24 | "narHash": "sha256-hYKMs3ilp09anGO7xzfGs3JqEgUqFMnZ8GMAqI6/k04=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "7282cb574e0607e65224d33be8241eae7cfe0979", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-25.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | # Candidate channels 4 | # - https://github.com/kachick/anylang-template/issues/17 5 | # - https://discourse.nixos.org/t/differences-between-nix-channels/13998 6 | # How to update the revision 7 | # - `nix flake update --commit-lock-file` # https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake-update.html 8 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; 9 | flake-utils.url = "github:numtide/flake-utils"; 10 | }; 11 | 12 | outputs = 13 | { 14 | self, 15 | nixpkgs, 16 | flake-utils, 17 | }: 18 | flake-utils.lib.eachDefaultSystem ( 19 | system: 20 | let 21 | pkgs = nixpkgs.legacyPackages.${system}; 22 | in 23 | rec { 24 | formatter = pkgs.nixfmt-tree; 25 | devShells.default = 26 | with pkgs; 27 | mkShell { 28 | env.NIX_PATH = "nixpkgs=${nixpkgs.outPath}"; 29 | buildInputs = [ 30 | # https://github.com/NixOS/nix/issues/730#issuecomment-162323824 31 | bashInteractive 32 | 33 | ruby_3_4 34 | # Required to build psych via irb dependency 35 | # https://github.com/kachick/irb-power_assert/issues/116 36 | # https://github.com/ruby/irb/pull/648 37 | libyaml 38 | 39 | dprint 40 | tree 41 | nixd 42 | nixfmt-rfc-style 43 | typos 44 | ]; 45 | }; 46 | 47 | packages.ruby-ulid = pkgs.stdenv.mkDerivation { 48 | name = "ruby-ulid"; 49 | src = self; 50 | installPhase = '' 51 | mkdir -p $out/bin 52 | cp -rf ./lib $out 53 | ''; 54 | runtimeDependencies = [ pkgs.ruby_3_4 ]; 55 | }; 56 | 57 | # `nix run` 58 | apps = { 59 | ruby = { 60 | type = "app"; 61 | program = 62 | with pkgs; 63 | lib.getExe (writeShellApplication { 64 | name = "ruby-with-ulid"; 65 | runtimeInputs = [ ruby_3_4 ]; 66 | text = '' 67 | ruby -r"${packages.ruby-ulid}/lib/ulid" "$@" 68 | ''; 69 | }); 70 | }; 71 | 72 | irb = { 73 | type = "app"; 74 | program = 75 | with pkgs; 76 | lib.getExe (writeShellApplication { 77 | name = "irb-with-ulid"; 78 | runtimeInputs = [ 79 | ruby_3_4 80 | libyaml 81 | ]; 82 | text = '' 83 | irb -r"${packages.ruby-ulid}/lib/ulid" "$@" 84 | ''; 85 | }); 86 | }; 87 | }; 88 | } 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /lib/ruby-ulid.rb: -------------------------------------------------------------------------------- 1 | # coding: us-ascii 2 | # frozen_string_literal: true 3 | # shareable_constant_value: literal 4 | 5 | # Copyright (C) 2021 Kenichi Kamiya 6 | 7 | require_relative('ulid') 8 | -------------------------------------------------------------------------------- /lib/ulid.rb: -------------------------------------------------------------------------------- 1 | # coding: us-ascii 2 | # frozen_string_literal: true 3 | 4 | # Copyright (C) 2021 Kenichi Kamiya 5 | 6 | require('securerandom') 7 | 8 | require_relative('ulid/version') 9 | require_relative('ulid/errors') 10 | require_relative('ulid/crockford_base32') 11 | require_relative('ulid/utils') 12 | require_relative('ulid/uuid') 13 | require_relative('ulid/monotonic_generator') 14 | 15 | # @see https://github.com/ulid/spec 16 | # @!attribute [r] milliseconds 17 | # @return [Integer] 18 | # @!attribute [r] entropy 19 | # @return [Integer] 20 | class ULID 21 | include(Comparable) 22 | 23 | TIMESTAMP_ENCODED_LENGTH = 10 24 | RANDOMNESS_ENCODED_LENGTH = 16 25 | ENCODED_LENGTH = 26 26 | 27 | OCTETS_LENGTH = 16 28 | 29 | MAX_MILLISECONDS = 281474976710655 30 | MAX_ENTROPY = 1208925819614629174706175 31 | MAX_INTEGER = 340282366920938463463374607431768211455 32 | 33 | # @see https://github.com/ulid/spec/pull/57 34 | # Currently not used as a constant, but kept as a reference for now. 35 | PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i 36 | 37 | STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i 38 | 39 | # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed. 40 | SCANNING_PATTERN = /\b[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}\b/i 41 | 42 | # Similar as Time#inspect since Ruby 2.7, however it is NOT same. 43 | # Time#inspect trancates needless digits. Keeping full milliseconds with "%3N" will fit for ULID. 44 | # @see https://bugs.ruby-lang.org/issues/15958 45 | # @see https://github.com/ruby/ruby/blob/744d17ff6c33b09334508e8110007ea2a82252f5/time.c#L4026-L4078 46 | TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z' 47 | 48 | RANDOM_INTEGER_GENERATOR = -> { 49 | SecureRandom.random_number(MAX_INTEGER) 50 | }.freeze 51 | 52 | Utils.make_sharable_constants(self) 53 | 54 | private_constant( 55 | :PATTERN_WITH_CROCKFORD_BASE32_SUBSET, 56 | :STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET, 57 | :SCANNING_PATTERN, 58 | :TIME_FORMAT_IN_INSPECT, 59 | :RANDOM_INTEGER_GENERATOR, 60 | :OCTETS_LENGTH, 61 | :UUID 62 | ) 63 | 64 | private_class_method(:new, :allocate) 65 | 66 | # @param [Integer, Time] moment 67 | # @param [Integer] entropy 68 | # @return [ULID] 69 | # @raise [OverflowError] if the given value is larger than the ULID limit 70 | # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number 71 | def self.generate(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy) 72 | milliseconds = Utils.milliseconds_from_moment(moment) 73 | base32hex = Utils.encode_base32hex(milliseconds:, entropy:) 74 | new( 75 | milliseconds:, 76 | entropy:, 77 | integer: Integer(base32hex, 32, exception: true), 78 | encoded: CrockfordBase32.from_base32hex(base32hex).freeze 79 | ) 80 | end 81 | 82 | # Almost same as [.generate] except directly returning String without needless object creation 83 | # 84 | # @param [Integer, Time] moment 85 | # @param [Integer] entropy 86 | # @return [String] 87 | def self.encode(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy) 88 | base32hex = Utils.encode_base32hex(milliseconds: Utils.milliseconds_from_moment(moment), entropy:) 89 | CrockfordBase32.from_base32hex(base32hex) 90 | end 91 | 92 | # Short hand of `ULID.generate(moment: time)` 93 | # @param [Time] time 94 | # @return [ULID] 95 | def self.at(time) 96 | raise(ArgumentError, 'ULID.at takes only `Time` instance') unless Time === time 97 | 98 | generate(moment: time) 99 | end 100 | 101 | # @param [Time, Integer] moment 102 | # @return [ULID] 103 | def self.min(moment=0) 104 | 0.equal?(moment) ? MIN : generate(moment:, entropy: 0) 105 | end 106 | 107 | # @param [Time, Integer] moment 108 | # @return [ULID] 109 | def self.max(moment=MAX_MILLISECONDS) 110 | MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment:, entropy: MAX_ENTROPY) 111 | end 112 | 113 | # @param [Range