ulid 1.2.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6d32b5f061971e975a65735d0d99c70c5c881e025d3e53325414d010f54d8f8
4
- data.tar.gz: 1dae11d7a1c57c7851ab29269d9fb7319c5af028e43ee0438a4b9d578fac7589
3
+ metadata.gz: 8cbdba2a6645c66ab28595928ff1121cb32b15de3a687b865678c00903e9379e
4
+ data.tar.gz: e5b1d117960ba3babdf3c73f067529f9f501bba90a7a8401625f0555c6b97b38
5
5
  SHA512:
6
- metadata.gz: fb53e67529dedec160c2fa2d80cd905874835ab545b2bfa769bc3fbf235f06e0c53051d90801ef11f1239efced45548820b5babd74b912f98af5d228d14c1995
7
- data.tar.gz: c1dcd2873822f7524b4644aee637f1f32e88736a816aa6d9dbe036cadf1480d1c92901fee6e98318977d08c697f8c16ef122ce37e78b0d12c805ca4fb6dd6771
6
+ metadata.gz: 07d641b18cabe49adb75117b744b03b442f5d9be7a97d433927265ec9cae1031e6ba51626860bcd2289be68a228971282e0ae45db37940697437c5a79c158d35
7
+ data.tar.gz: 823d8b48de6d9eda3eaad2955384c8d9fc4251f8c1b447ec7c743c731d6dd5c942880d9e93aacd97ea7c9f66cc755e1f3ab2031307377be24a5abd2fb262606f
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [rafaelsales]
@@ -1,20 +1,26 @@
1
1
  name: Ruby
2
2
 
3
- on: [push]
3
+ on:
4
+ pull_request: ~
5
+ push:
6
+ branches:
7
+ - master
4
8
 
5
9
  jobs:
6
10
  build:
7
11
 
8
12
  runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2']
9
16
 
10
17
  steps:
11
18
  - uses: actions/checkout@v2
12
- - name: Set up Ruby 2.6
13
- uses: actions/setup-ruby@v1
19
+ - name: Set up Ruby
20
+ uses: ruby/setup-ruby@v1
14
21
  with:
15
- ruby-version: 2.6.x
16
- - name: Build and test with Rake
22
+ ruby-version: ${{ matrix.ruby-version }}
23
+ bundler-cache: true # Run "bundle install", and cache the result automatically.
24
+ - name: Run tests
17
25
  run: |
18
- gem install bundler
19
- bundle install --jobs 4 --retry 3
20
26
  bundle exec rake test
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 1.4.0
2
+
3
+ - PR #32 - Allow fully deterministic ULID generation by providing both the timestamp
4
+ component and a string suffix to replace the randomness component. Thanks @andjosh
5
+
1
6
  # 1.2.0
2
7
 
3
8
  - PR #20 - Use an array to improve speed / reduce memory allocations. Thanks, @jamescook
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :development, :test do
6
- gem 'pry-byebug'
6
+ gem 'byebug'
7
7
  gem 'rake'
8
8
  gem 'rubocop'
9
9
  end
data/Gemfile.lock CHANGED
@@ -1,50 +1,48 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ulid (1.1.1)
4
+ ulid (1.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- ast (2.3.0)
9
+ ast (2.4.2)
10
10
  base32-crockford (0.1.0)
11
- byebug (9.1.0)
12
- coderay (1.1.2)
13
- method_source (0.9.0)
14
- minitest (5.10.3)
15
- parallel (1.12.0)
16
- parser (2.4.0.0)
17
- ast (~> 2.2)
18
- powerpack (0.1.1)
19
- pry (0.11.3)
20
- coderay (~> 1.1.0)
21
- method_source (~> 0.9.0)
22
- pry-byebug (3.5.1)
23
- byebug (~> 9.1)
24
- pry (~> 0.10)
25
- rainbow (2.2.2)
26
- rake
27
- rake (10.5.0)
28
- rubocop (0.50.0)
11
+ byebug (11.1.3)
12
+ json (2.6.3)
13
+ minitest (5.18.0)
14
+ parallel (1.22.1)
15
+ parser (3.2.1.0)
16
+ ast (~> 2.4.1)
17
+ rainbow (3.1.1)
18
+ rake (13.0.6)
19
+ regexp_parser (2.7.0)
20
+ rexml (3.2.5)
21
+ rubocop (1.47.0)
22
+ json (~> 2.3)
29
23
  parallel (~> 1.10)
30
- parser (>= 2.3.3.1, < 3.0)
31
- powerpack (~> 0.1)
32
- rainbow (>= 2.2.2, < 3.0)
24
+ parser (>= 3.2.0.0)
25
+ rainbow (>= 2.2.2, < 4.0)
26
+ regexp_parser (>= 1.8, < 3.0)
27
+ rexml (>= 3.2.5, < 4.0)
28
+ rubocop-ast (>= 1.26.0, < 2.0)
33
29
  ruby-progressbar (~> 1.7)
34
- unicode-display_width (~> 1.0, >= 1.0.1)
35
- ruby-progressbar (1.8.3)
36
- unicode-display_width (1.3.0)
30
+ unicode-display_width (>= 2.4.0, < 3.0)
31
+ rubocop-ast (1.27.0)
32
+ parser (>= 3.2.1.0)
33
+ ruby-progressbar (1.13.0)
34
+ unicode-display_width (2.4.2)
37
35
 
38
36
  PLATFORMS
39
37
  ruby
40
38
 
41
39
  DEPENDENCIES
42
40
  base32-crockford
41
+ byebug
43
42
  minitest
44
- pry-byebug
45
43
  rake
46
44
  rubocop
47
45
  ulid!
48
46
 
49
47
  BUNDLED WITH
50
- 2.0.1
48
+ 2.4.7
data/README.md CHANGED
@@ -48,6 +48,30 @@ require 'ulid'
48
48
  ULID.generate # 01ARZ3NDEKTSV4RRFFQ69G5FAV
49
49
  ```
50
50
 
51
+ **I want to generate a ULID using an arbitrary timestamp**
52
+
53
+ You can optionally pass a `Time` instance to `ULID.generate` to set an arbitrary timestamp component, i.e. the prefix of the ULID.
54
+
55
+ ```ruby
56
+ time_t1 = Time.now
57
+ ulid = ULID.generate(time_t1)
58
+ ```
59
+
60
+ **I want to generate a ULID using an arbitrary suffix, i.e. without the randomness component**
61
+
62
+ You can optionally pass a 80-bit hex-encodable `String` on the argument `suffix` to `ULID.generate`. This will replace the randomness component
63
+ by the suffix provided. This allows for fully deterministic ULIDs.
64
+
65
+ ```ruby
66
+ require 'securerandom'
67
+
68
+ time = Time.now
69
+ an_event_identifier = SecureRandom.uuid
70
+ ulid1 = ULID.generate(time, suffix: an_event_identifier)
71
+ ulid2 = ULID.generate(time, suffix: an_event_identifier)
72
+ ulid1 == ulid2 # true
73
+ ```
74
+
51
75
  ## Specification
52
76
 
53
77
  Below is the current specification of ULID as implemented in this repository. *Note: the binary format has not been implemented.*
@@ -113,4 +137,4 @@ bundle exec rake test
113
137
 
114
138
  ### Credits and references:
115
139
 
116
- * https://github.com/alizain/ulid
140
+ * https://github.com/ulid/javascript
@@ -1,59 +1,74 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- if RUBY_VERSION >= '2.5'
4
- require 'securerandom'
5
- else
6
- require 'sysrandom/securerandom'
7
- end
3
+ require 'securerandom'
8
4
 
9
5
  module ULID
10
6
  module Generator
11
7
  ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.bytes.freeze # Crockford's Base32
12
- RANDOM_BYTES = 10
8
+ RANDOM_BITS = 80
13
9
  ENCODED_LENGTH = 26
14
- BIT_LENGTH = 128
15
10
  BITS_PER_B32_CHAR = 5
16
11
  ZERO = '0'.ord
17
-
18
12
  MASK = 0x1f
19
13
 
20
- def generate(time = Time.now)
21
- input = octo_word(time)
14
+ # Generates a 128-bit ULID string.
15
+ # @param [Time] time (Time.now) Timestamp - first 48 bits
16
+ # @param [String] suffix (80 random bits) - the remaining 80 bits as hex encodable string
17
+ def generate(time = Time.now, suffix: nil)
18
+ (hi, lo) = generate_bytes(time, suffix: suffix).unpack('Q>Q>')
19
+ if hi.nil? || lo.nil?
20
+ raise ArgumentError, 'suffix string without hex encoding passed to ULID generator'
21
+ end
22
22
 
23
- encode(input, ENCODED_LENGTH)
23
+ integer = (hi << 64) | lo
24
+ encode(integer, ENCODED_LENGTH)
24
25
  end
25
26
 
26
- def generate_bytes(time = Time.now)
27
- time_48bit(time) + random_bytes
27
+ # Generates a 128-bit ULID.
28
+ # @param [Time] time (Time.now) Timestamp - first 48 bits
29
+ # @param [String] suffix (80 random bits) - the remaining 80 bits as hex encodable string
30
+ def generate_bytes(time = Time.now, suffix: nil)
31
+ suffix_bytes =
32
+ if suffix
33
+ suffix.split('').map { |char| char.to_i(32) }.pack('C*')
34
+ else
35
+ SecureRandom.random_bytes(RANDOM_BITS / 8)
36
+ end
37
+
38
+ time_48bit(time) + suffix_bytes
28
39
  end
29
40
 
30
41
  private
31
42
 
43
+ # Encodes a 128-bit integer input as a 26-character ULID string using Crockford's Base32.
32
44
  def encode(input, length)
33
- e = Array.new(length, ZERO)
45
+ encoded = Array.new(length, ZERO)
34
46
  i = length - 1
35
47
 
36
48
  while input > 0
37
- e[i] = ENCODING[input & MASK]
38
- input >>= 5
49
+ encoded[i] = ENCODING[input & MASK]
50
+ input >>= BITS_PER_B32_CHAR
39
51
  i -= 1
40
52
  end
41
53
 
42
- e.pack('c*')
43
- end
44
-
45
- def octo_word(time = Time.now)
46
- (hi, lo) = generate_bytes(time).unpack('Q>Q>')
47
- (hi << 64) | lo
54
+ encoded.pack('c*')
48
55
  end
49
56
 
57
+ # Returns the first 6 bytes of a timestamp (in milliseconds since the Unix epoch) as a 48-bit byte string
50
58
  def time_48bit(time = Time.now)
51
- time_ms = (time.to_f * 1000).to_i
59
+ # Avoid `time.to_f` since we want to accurately represent a whole number of milliseconds:
60
+ #
61
+ # > time = Time.new(2020, 1, 5, 7, 3, Rational(2, 1000))
62
+ # => 2020-01-05 07:03:00 +0000
63
+ # > (time.to_f * 1000).to_i
64
+ # => 1578207780001
65
+ #
66
+ # vs
67
+ #
68
+ # > (time.to_r * 1000).to_i
69
+ # => 1578207780002
70
+ time_ms = (time.to_r * 1000).to_i
52
71
  [time_ms].pack('Q>')[2..-1]
53
72
  end
54
-
55
- def random_bytes
56
- SecureRandom.random_bytes(RANDOM_BYTES)
57
- end
58
73
  end
59
74
  end
data/lib/ulid/version.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen-string-literal: true
1
2
  module ULID
2
- VERSION = '1.2.0'.freeze
3
+ VERSION = '1.4.0'
3
4
  end
@@ -6,7 +6,7 @@ describe ULID do
6
6
  it 'ensures it has 26 chars' do
7
7
  ulid = ULID.generate
8
8
 
9
- ulid.length.must_equal 26
9
+ assert_equal ulid.length, 26
10
10
  end
11
11
 
12
12
  it 'is sortable' do
@@ -25,10 +25,65 @@ describe ULID do
25
25
 
26
26
  it 'encodes the timestamp in the first 10 characters' do
27
27
  # test case taken from original ulid README:
28
- # https://github.com/alizain/ulid#seed-time
29
- ulid = ULID.generate(Time.at(1_469_918_176.385))
28
+ # https://github.com/ulid/javascript#seed-time
29
+ #
30
+ # N.b. we avoid specifying the time as a float, since we lose precision:
31
+ #
32
+ # > Time.at(1_469_918_176.385).strftime("%F %T.%N")
33
+ # => "2016-07-30 23:36:16.384999990"
34
+ #
35
+ # vs the correct:
36
+ #
37
+ # > Time.at(1_469_918_176, 385, :millisecond).strftime("%F %T.%N")
38
+ # => "2016-07-30 23:36:16.385000000"
39
+ ulid = ULID.generate(Time.at(1_469_918_176, 385, :millisecond))
30
40
  assert_equal '01ARYZ6S41', ulid[0...10]
31
41
  end
42
+
43
+ it 'respects millisecond-precision order' do
44
+ ulids = Array.new(1000) do |millis|
45
+ time = Time.new(2020, 1, 2, 3, 4, Rational(millis, 10**3))
46
+
47
+ ULID.generate(time)
48
+ end
49
+
50
+ assert_equal(ulids, ulids.sort)
51
+ end
52
+
53
+ it 'is deterministic based on time' do
54
+ input_time = Time.now
55
+ ulid1 = ULID.generate(input_time)
56
+ ulid2 = ULID.generate(input_time)
57
+ assert_equal ulid2.slice(0, 10), ulid1.slice(0, 10)
58
+ assert ulid2 != ulid1
59
+ end
60
+
61
+ it 'is deterministic based on suffix' do
62
+ input_time = Time.now
63
+ suffix = SecureRandom.uuid
64
+ ulid1 = ULID.generate(input_time, suffix: suffix)
65
+ ulid2 = ULID.generate(input_time + 1, suffix: suffix)
66
+ assert_equal ulid2.slice(10, 26), ulid1.slice(10, 26)
67
+ assert ulid2 != ulid1
68
+ end
69
+
70
+ it 'is fully deterministic based on time and suffix' do
71
+ input_time = Time.now
72
+ suffix = SecureRandom.uuid
73
+ ulid1 = ULID.generate(input_time, suffix: suffix)
74
+ ulid2 = ULID.generate(input_time, suffix: suffix)
75
+ assert_equal ulid2, ulid1
76
+ end
77
+
78
+ it 'raises exception when non-encodable 80-bit suffix string is used' do
79
+ input_time = Time.now
80
+ suffix = SecureRandom.uuid
81
+ assert_raises(ArgumentError) do
82
+ ULID.generate(input_time, suffix: suffix[0...9])
83
+ end
84
+
85
+ ULID.generate(input_time, suffix: suffix[0...10])
86
+ end
32
87
  end
33
88
 
34
89
  describe 'underlying binary' do
@@ -41,8 +96,8 @@ describe ULID do
41
96
  end
42
97
 
43
98
  it 'encodes the remaining 80 bits as random' do
44
- random_bytes = SecureRandom.random_bytes(ULID::Generator::RANDOM_BYTES)
45
- ULID.stub(:random_bytes, random_bytes) do
99
+ random_bytes = SecureRandom.random_bytes(ULID::Generator::RANDOM_BITS / 8)
100
+ SecureRandom.stub(:random_bytes, random_bytes) do
46
101
  bytes = ULID.generate_bytes
47
102
  assert bytes[6..-1] == random_bytes
48
103
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'pry'
1
+ require 'byebug'
2
2
  require 'minitest/autorun'
3
3
  require 'minitest/pride'
4
4
  require 'ulid'
data/ulid.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  lib = File.expand_path('lib', __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'ulid/version'
3
+ require_relative 'lib/ulid/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'ulid'
@@ -12,12 +12,6 @@ Gem::Specification.new do |spec|
12
12
  spec.license = 'MIT'
13
13
 
14
14
  spec.files = `git ls-files -z`.split("\x0")
15
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
15
  spec.require_paths = ['lib']
18
-
19
- spec.post_install_message = '
20
- ulid gem needs to install sysrandom gem if you use Ruby 2.4 or older.
21
- Execute `gem install sysrandom` or add `gem "sysrandom"` to Gemfile.
22
- '
16
+ spec.required_ruby_version = '>= 2.6.8'
23
17
  end
metadata CHANGED
@@ -1,22 +1,23 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ulid
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Sales
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-03 00:00:00.000000000 Z
11
+ date: 2023-03-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description:
14
14
  email:
15
15
  - rafaelcds@gmail.com
16
16
  executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - ".github/FUNDING.yml"
20
21
  - ".github/workflows/ruby.yml"
21
22
  - ".gitignore"
22
23
  - ".rubocop.yml"
@@ -37,10 +38,7 @@ homepage: https://github.com/rafaelsales/ulid
37
38
  licenses:
38
39
  - MIT
39
40
  metadata: {}
40
- post_install_message: |2
41
-
42
- ulid gem needs to install sysrandom gem if you use Ruby 2.4 or older.
43
- Execute `gem install sysrandom` or add `gem "sysrandom"` to Gemfile.
41
+ post_install_message:
44
42
  rdoc_options: []
45
43
  require_paths:
46
44
  - lib
@@ -48,19 +46,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
48
46
  requirements:
49
47
  - - ">="
50
48
  - !ruby/object:Gem::Version
51
- version: '0'
49
+ version: 2.6.8
52
50
  required_rubygems_version: !ruby/object:Gem::Requirement
53
51
  requirements:
54
52
  - - ">="
55
53
  - !ruby/object:Gem::Version
56
54
  version: '0'
57
55
  requirements: []
58
- rubyforge_project:
59
- rubygems_version: 2.7.6.2
60
- signing_key:
56
+ rubygems_version: 3.3.7
57
+ signing_key:
61
58
  specification_version: 4
62
59
  summary: Universally Unique Lexicographically Sortable Identifier implementation for
63
60
  Ruby
64
- test_files:
65
- - spec/lib/ulid_spec.rb
66
- - spec/spec_helper.rb
61
+ test_files: []