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 +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/ruby.yml +13 -7
- data/CHANGELOG.md +5 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +26 -28
- data/README.md +25 -1
- data/lib/ulid/generator.rb +42 -27
- data/lib/ulid/version.rb +2 -1
- data/spec/lib/ulid_spec.rb +60 -5
- data/spec/spec_helper.rb +1 -1
- data/ulid.gemspec +2 -8
- metadata +10 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8cbdba2a6645c66ab28595928ff1121cb32b15de3a687b865678c00903e9379e
|
4
|
+
data.tar.gz: e5b1d117960ba3babdf3c73f067529f9f501bba90a7a8401625f0555c6b97b38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 07d641b18cabe49adb75117b744b03b442f5d9be7a97d433927265ec9cae1031e6ba51626860bcd2289be68a228971282e0ae45db37940697437c5a79c158d35
|
7
|
+
data.tar.gz: 823d8b48de6d9eda3eaad2955384c8d9fc4251f8c1b447ec7c743c731d6dd5c942880d9e93aacd97ea7c9f66cc755e1f3ab2031307377be24a5abd2fb262606f
|
data/.github/FUNDING.yml
ADDED
data/.github/workflows/ruby.yml
CHANGED
@@ -1,20 +1,26 @@
|
|
1
1
|
name: Ruby
|
2
2
|
|
3
|
-
on:
|
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
|
13
|
-
uses:
|
19
|
+
- name: Set up Ruby
|
20
|
+
uses: ruby/setup-ruby@v1
|
14
21
|
with:
|
15
|
-
ruby-version:
|
16
|
-
|
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
data/Gemfile.lock
CHANGED
@@ -1,50 +1,48 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ulid (1.
|
4
|
+
ulid (1.3.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
ast (2.
|
9
|
+
ast (2.4.2)
|
10
10
|
base32-crockford (0.1.0)
|
11
|
-
byebug (
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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 (>=
|
31
|
-
|
32
|
-
|
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 (
|
35
|
-
|
36
|
-
|
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.
|
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/
|
140
|
+
* https://github.com/ulid/javascript
|
data/lib/ulid/generator.rb
CHANGED
@@ -1,59 +1,74 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
23
|
+
integer = (hi << 64) | lo
|
24
|
+
encode(integer, ENCODED_LENGTH)
|
24
25
|
end
|
25
26
|
|
26
|
-
|
27
|
-
|
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
|
-
|
45
|
+
encoded = Array.new(length, ZERO)
|
34
46
|
i = length - 1
|
35
47
|
|
36
48
|
while input > 0
|
37
|
-
|
38
|
-
input >>=
|
49
|
+
encoded[i] = ENCODING[input & MASK]
|
50
|
+
input >>= BITS_PER_B32_CHAR
|
39
51
|
i -= 1
|
40
52
|
end
|
41
53
|
|
42
|
-
|
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
|
-
|
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
data/spec/lib/ulid_spec.rb
CHANGED
@@ -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
|
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/
|
29
|
-
|
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::
|
45
|
-
|
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
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
|
-
|
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.
|
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:
|
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:
|
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:
|
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
|
-
|
59
|
-
|
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: []
|