ruby-ulid 0.0.6
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 +7 -0
- data/lib/ulid/version.rb +6 -0
- data/lib/ulid.rb +237 -0
- data/sig/ulid.rbs +74 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e00e5eba399c7d96e26ada6a0f0463fdb7fc08d7ec69c96588a5f85320a997be
|
|
4
|
+
data.tar.gz: e5f79b9a86ab038be2d0b39a787d8e70108998d3a024c699e401770ec6edade3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0b4221414e2cdeeb67f6f6173c5ce25475f388e11df12c16864f576d76cac1fe3197dc5c2dd6ddef03d4fdfe6c079e0256a91c7b1728af73dd9d1b56c6ca7109
|
|
7
|
+
data.tar.gz: 369838ea1deac94dc44c71a37a606eb57a88f0266910f555a3ede1b7d85653be564499f7bf009cdd426a4a84a4be7e4d0f046ca4106bf25e8613cede927a47c5
|
data/lib/ulid/version.rb
ADDED
data/lib/ulid.rb
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# coding: us-ascii
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
|
4
|
+
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'singleton'
|
|
7
|
+
require 'integer/base'
|
|
8
|
+
require_relative 'ulid/version'
|
|
9
|
+
|
|
10
|
+
# @see https://github.com/ulid/spec
|
|
11
|
+
class ULID
|
|
12
|
+
include Comparable
|
|
13
|
+
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
class OverflowError < Error; end
|
|
16
|
+
class ParserError < Error; end
|
|
17
|
+
|
|
18
|
+
# Crockford's Base32. Excluded I, L, O, U.
|
|
19
|
+
# @see https://www.crockford.com/base32.html
|
|
20
|
+
ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.chars.map(&:freeze).freeze
|
|
21
|
+
|
|
22
|
+
TIME_PART_LENGTH = 10
|
|
23
|
+
RANDOMNESS_PART_LENGTH = 16
|
|
24
|
+
ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
|
|
25
|
+
TIME_OCTETS_LENGTH = 6
|
|
26
|
+
RANDOMNESS_OCTETS_LENGTH = 10
|
|
27
|
+
OCTETS_LENGTH = TIME_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
|
|
28
|
+
MAX_MILLISECONDS = 281474976710655
|
|
29
|
+
MAX_ENTROPY = 1208925819614629174706175
|
|
30
|
+
|
|
31
|
+
# Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
|
|
32
|
+
# @see https://bugs.ruby-lang.org/issues/15958
|
|
33
|
+
TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
|
|
34
|
+
|
|
35
|
+
class MonotonicGenerator
|
|
36
|
+
include Singleton
|
|
37
|
+
|
|
38
|
+
attr_accessor :latest_milliseconds, :latest_entropy
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
reset
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [ULID]
|
|
45
|
+
def generate
|
|
46
|
+
milliseconds = ULID.current_milliseconds
|
|
47
|
+
reasonable_entropy = ULID.reasonable_entropy
|
|
48
|
+
|
|
49
|
+
@latest_milliseconds ||= milliseconds
|
|
50
|
+
@latest_entropy ||= reasonable_entropy
|
|
51
|
+
if @latest_milliseconds != milliseconds
|
|
52
|
+
@latest_milliseconds = milliseconds
|
|
53
|
+
@latest_entropy = reasonable_entropy
|
|
54
|
+
else
|
|
55
|
+
@latest_entropy += 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
ULID.new milliseconds: milliseconds, entropy: @latest_entropy
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [self]
|
|
62
|
+
def reset
|
|
63
|
+
@latest_milliseconds = nil
|
|
64
|
+
@latest_entropy = nil
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [void]
|
|
69
|
+
def freeze
|
|
70
|
+
raise TypeError, "cannot freeze #{self.class}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
MONOTONIC_GENERATOR = MonotonicGenerator.instance
|
|
75
|
+
|
|
76
|
+
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :MonotonicGenerator
|
|
77
|
+
|
|
78
|
+
# @param [Integer, Time] moment
|
|
79
|
+
# @param [Integer] entropy
|
|
80
|
+
# @return [ULID]
|
|
81
|
+
def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
|
|
82
|
+
milliseconds = moment.kind_of?(Time) ? (moment.to_r * 1000).to_i : moment.to_int
|
|
83
|
+
new milliseconds: milliseconds, entropy: entropy
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [ULID]
|
|
87
|
+
def self.monotonic_generate
|
|
88
|
+
MONOTONIC_GENERATOR.generate
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Integer]
|
|
92
|
+
def self.current_milliseconds
|
|
93
|
+
time_to_milliseconds(Time.now)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param [Time] time
|
|
97
|
+
# @return [Integer]
|
|
98
|
+
def self.time_to_milliseconds(time)
|
|
99
|
+
(time.to_r * 1000).to_i
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Integer]
|
|
103
|
+
def self.reasonable_entropy
|
|
104
|
+
SecureRandom.random_number(MAX_ENTROPY)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @param [String, #to_str] string
|
|
108
|
+
# @return [ULID]
|
|
109
|
+
def self.parse(string)
|
|
110
|
+
begin
|
|
111
|
+
string = string.to_str
|
|
112
|
+
unless string.size == ENCODED_ID_LENGTH
|
|
113
|
+
raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
|
|
114
|
+
end
|
|
115
|
+
timestamp = string.slice(0, TIME_PART_LENGTH)
|
|
116
|
+
randomness = string.slice(TIME_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
|
117
|
+
milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
|
|
118
|
+
entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
|
|
119
|
+
rescue => err
|
|
120
|
+
raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
new milliseconds: milliseconds, entropy: entropy
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @param [String] string
|
|
127
|
+
# @return [Boolean]
|
|
128
|
+
def self.valid?(string)
|
|
129
|
+
parse(string)
|
|
130
|
+
rescue Exception
|
|
131
|
+
false
|
|
132
|
+
else
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
attr_reader :milliseconds, :entropy
|
|
137
|
+
|
|
138
|
+
def initialize(milliseconds:, entropy:)
|
|
139
|
+
milliseconds = milliseconds.to_int
|
|
140
|
+
entropy = entropy.to_int
|
|
141
|
+
raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
|
|
142
|
+
raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
|
|
143
|
+
raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
|
|
144
|
+
|
|
145
|
+
@milliseconds = milliseconds
|
|
146
|
+
@entropy = entropy
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @return [String]
|
|
150
|
+
def to_str
|
|
151
|
+
@string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
|
|
152
|
+
end
|
|
153
|
+
alias_method :to_s, :to_str
|
|
154
|
+
|
|
155
|
+
# @return [Integer]
|
|
156
|
+
def to_i
|
|
157
|
+
@integer ||= inverse_of_digits(octets)
|
|
158
|
+
end
|
|
159
|
+
alias_method :hash, :to_i
|
|
160
|
+
|
|
161
|
+
# @return [Integer, nil]
|
|
162
|
+
def <=>(other)
|
|
163
|
+
other.kind_of?(self.class) ? (to_i <=> other.to_i) : nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [String]
|
|
167
|
+
def inspect
|
|
168
|
+
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @return [Boolean]
|
|
172
|
+
def eql?(other)
|
|
173
|
+
other.equal?(self) || (other.kind_of?(self.class) && other.to_i == to_i)
|
|
174
|
+
end
|
|
175
|
+
alias_method :==, :eql?
|
|
176
|
+
|
|
177
|
+
# @return [Time]
|
|
178
|
+
def to_time
|
|
179
|
+
@time ||= Time.at(0, @milliseconds, :millisecond).utc
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @return [Array<Integer>]
|
|
183
|
+
def octets
|
|
184
|
+
@octets ||= (time_octets + randomness_octets).freeze
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [Array<Integer>]
|
|
188
|
+
def time_octets
|
|
189
|
+
@time_octets ||= octets_from_integer(@milliseconds, length: TIME_OCTETS_LENGTH).freeze
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @return [Array<Integer>]
|
|
193
|
+
def randomness_octets
|
|
194
|
+
@randomness_octets ||= octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# @return [ULID]
|
|
198
|
+
def next
|
|
199
|
+
@next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
|
|
200
|
+
end
|
|
201
|
+
alias_method :succ, :next
|
|
202
|
+
|
|
203
|
+
# @return [self]
|
|
204
|
+
def freeze
|
|
205
|
+
# Evaluate all caching
|
|
206
|
+
inspect
|
|
207
|
+
octets
|
|
208
|
+
succ
|
|
209
|
+
to_i
|
|
210
|
+
super
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
# @param [Integer] integer
|
|
216
|
+
# @param [Integer] length
|
|
217
|
+
# @return [Array<Integer>]
|
|
218
|
+
def octets_from_integer(integer, length:)
|
|
219
|
+
digits = integer.digits(256)
|
|
220
|
+
(length - digits.size).times do
|
|
221
|
+
digits.push 0
|
|
222
|
+
end
|
|
223
|
+
digits.reverse!
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
|
|
227
|
+
# @param [Array<Integer>] reversed_digits
|
|
228
|
+
# @return [Integer]
|
|
229
|
+
def inverse_of_digits(reversed_digits)
|
|
230
|
+
base = 256
|
|
231
|
+
num = 0
|
|
232
|
+
reversed_digits.each do |digit|
|
|
233
|
+
num = (num * base) + digit
|
|
234
|
+
end
|
|
235
|
+
num
|
|
236
|
+
end
|
|
237
|
+
end
|
data/sig/ulid.rbs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Classes
|
|
2
|
+
class ULID
|
|
3
|
+
VERSION: String
|
|
4
|
+
ENCODING_CHARS: Array[String]
|
|
5
|
+
TIME_PART_LENGTH: Integer
|
|
6
|
+
RANDOMNESS_PART_LENGTH: Integer
|
|
7
|
+
ENCODED_ID_LENGTH: Integer
|
|
8
|
+
TIME_OCTETS_LENGTH: Integer
|
|
9
|
+
RANDOMNESS_OCTETS_LENGTH: Integer
|
|
10
|
+
OCTETS_LENGTH: Integer
|
|
11
|
+
MAX_MILLISECONDS: Integer
|
|
12
|
+
MAX_ENTROPY: Integer
|
|
13
|
+
TIME_FORMAT_IN_INSPECT: String
|
|
14
|
+
MONOTONIC_GENERATOR: MonotonicGenerator
|
|
15
|
+
include Comparable
|
|
16
|
+
@string: String
|
|
17
|
+
@integer: Integer
|
|
18
|
+
@octets: Array[Integer]
|
|
19
|
+
@time_octets: Array[Integer]
|
|
20
|
+
@randomness_octets: Array[Integer]
|
|
21
|
+
@inspect: String
|
|
22
|
+
@time: Time
|
|
23
|
+
@next: ULID
|
|
24
|
+
|
|
25
|
+
def self.generate: (?moment: Time | Integer, ?entropy: Integer) -> ULID
|
|
26
|
+
def self.monotonic_generate: -> ULID
|
|
27
|
+
def self.current_milliseconds: -> Integer
|
|
28
|
+
def self.time_to_milliseconds: (Time time) -> Integer
|
|
29
|
+
def self.reasonable_entropy: -> Integer
|
|
30
|
+
def self.parse: (String string) -> ULID
|
|
31
|
+
def self.valid?: (untyped string) -> bool
|
|
32
|
+
attr_reader milliseconds: Integer
|
|
33
|
+
attr_reader entropy: Integer
|
|
34
|
+
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
|
35
|
+
def to_str: -> String
|
|
36
|
+
def to_s: -> String
|
|
37
|
+
def to_i: -> Integer
|
|
38
|
+
def hash: -> Integer
|
|
39
|
+
def <=>: (untyped other) -> Integer?
|
|
40
|
+
def inspect: -> String
|
|
41
|
+
def eql?: (untyped other) -> bool
|
|
42
|
+
def ==: (untyped other) -> bool
|
|
43
|
+
def to_time: -> Time
|
|
44
|
+
def octets: -> Array[Integer]
|
|
45
|
+
def time_octets: -> Array[Integer]
|
|
46
|
+
def randomness_octets: -> Array[Integer]
|
|
47
|
+
def next: -> ULID
|
|
48
|
+
def succ: -> ULID
|
|
49
|
+
def freeze: -> self
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
def octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
|
|
53
|
+
def inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
|
|
54
|
+
|
|
55
|
+
class Error < StandardError
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class OverflowError < Error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class ParserError < Error
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class MonotonicGenerator
|
|
65
|
+
include Singleton
|
|
66
|
+
|
|
67
|
+
attr_accessor latest_milliseconds: Integer?
|
|
68
|
+
attr_accessor latest_entropy: Integer?
|
|
69
|
+
def initialize: -> void
|
|
70
|
+
def generate: -> ULID
|
|
71
|
+
def reset: -> void
|
|
72
|
+
def freeze: -> void
|
|
73
|
+
end
|
|
74
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby-ulid
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.6
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kenichi Kamiya
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-04-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: integer-base
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.1.2
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.1.2
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: test-unit
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 3.4.1
|
|
34
|
+
- - "<"
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '4'
|
|
37
|
+
type: :development
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 3.4.1
|
|
44
|
+
- - "<"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '4'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: benchmark-ips
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 2.8.4
|
|
54
|
+
- - "<"
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '3'
|
|
57
|
+
type: :development
|
|
58
|
+
prerelease: false
|
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 2.8.4
|
|
64
|
+
- - "<"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: yard
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 0.9.26
|
|
74
|
+
- - "<"
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '2'
|
|
77
|
+
type: :development
|
|
78
|
+
prerelease: false
|
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 0.9.26
|
|
84
|
+
- - "<"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '2'
|
|
87
|
+
description: |2
|
|
88
|
+
ULID(Universally Unique Lexicographically Sortable Identifier) is defined on https://github.com/ulid/spec.
|
|
89
|
+
It has useful specs for actual applications.
|
|
90
|
+
This gem aims to provide the generator, monotonic generator, parser and handy manipulation methods for the ID.
|
|
91
|
+
Also having rbs signature files.
|
|
92
|
+
email:
|
|
93
|
+
- kachick1+ruby@gmail.com
|
|
94
|
+
executables: []
|
|
95
|
+
extensions: []
|
|
96
|
+
extra_rdoc_files: []
|
|
97
|
+
files:
|
|
98
|
+
- lib/ulid.rb
|
|
99
|
+
- lib/ulid/version.rb
|
|
100
|
+
- sig/ulid.rbs
|
|
101
|
+
homepage: https://github.com/kachick/ruby-ulid
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
metadata:
|
|
105
|
+
documentation_uri: https://kachick.github.io/ruby-ulid/
|
|
106
|
+
homepage_uri: https://github.com/kachick/ruby-ulid
|
|
107
|
+
source_code_uri: https://github.com/kachick/ruby-ulid
|
|
108
|
+
post_install_message:
|
|
109
|
+
rdoc_options: []
|
|
110
|
+
require_paths:
|
|
111
|
+
- lib
|
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '2.5'
|
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ">="
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '0'
|
|
122
|
+
requirements: []
|
|
123
|
+
rubygems_version: 3.2.15
|
|
124
|
+
signing_key:
|
|
125
|
+
specification_version: 4
|
|
126
|
+
summary: A handy ULID library
|
|
127
|
+
test_files: []
|