ulid 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []