ruby-ulid 0.0.6 → 0.0.11

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: e00e5eba399c7d96e26ada6a0f0463fdb7fc08d7ec69c96588a5f85320a997be
4
- data.tar.gz: e5f79b9a86ab038be2d0b39a787d8e70108998d3a024c699e401770ec6edade3
3
+ metadata.gz: 91f6d4c9dad8e12663686099c487dbbf3ec5b7995e0e4b2b210ac8a16fbcd91c
4
+ data.tar.gz: 1ca6e29d83771636b2a20aa71d90f15699d07d6a7a75f22c306094507bb51d89
5
5
  SHA512:
6
- metadata.gz: 0b4221414e2cdeeb67f6f6173c5ce25475f388e11df12c16864f576d76cac1fe3197dc5c2dd6ddef03d4fdfe6c079e0256a91c7b1728af73dd9d1b56c6ca7109
7
- data.tar.gz: 369838ea1deac94dc44c71a37a606eb57a88f0266910f555a3ede1b7d85653be564499f7bf009cdd426a4a84a4be7e4d0f046ca4106bf25e8613cede927a47c5
6
+ metadata.gz: 22dcc2541f7e4fa1a1d4014f996aef3d3bfcb69b002c1cc6ca4864e17bce50a327e1ec98453b4dab0773400b678055dc024a7287342a08916ce30772aadb418d
7
+ data.tar.gz: 27c69c7319858a4c732f04f6944ff180f3dfc9a5df12bd67cd1e4cac91421a2bc00d1a79c8ff91190b425731f831fdaef1a2f5bc0c3ef4fdaff73a6ce6749e35
data/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
data/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # ruby-ulid
2
+
3
+ A handy `ULID` library
4
+
5
+ The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
6
+ This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
7
+ Also providing rbs signature files.
8
+
9
+ ---
10
+
11
+ ![ULIDlogo](https://raw.githubusercontent.com/kachick/ruby-ulid/main/logo.png)
12
+
13
+ ![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test.yml/badge.svg?branch=main)
14
+ [![Gem Version](https://badge.fury.io/rb/ruby-ulid.png)](http://badge.fury.io/rb/ruby-ulid)
15
+
16
+ ## Universally Unique Lexicographically Sortable Identifier
17
+
18
+ UUID can be suboptimal for many uses-cases because:
19
+
20
+ - It isn't the most character efficient way of encoding 128 bits of randomness
21
+ - UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address
22
+ - UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures
23
+ - UUID v4 provides no other information than randomness which can cause fragmentation in many data structures
24
+
25
+ Instead, herein is proposed ULID:
26
+
27
+ - 128-bit compatibility with UUID
28
+ - 1.21e+24 unique ULIDs per millisecond
29
+ - Lexicographically sortable!
30
+ - Canonically encoded as a 26 character string, as opposed to the 36 character UUID
31
+ - Uses Crockford's base32 for better efficiency and readability (5 bits per character)
32
+ - Case insensitive
33
+ - No special characters (URL safe)
34
+ - Monotonic sort order (correctly detects and handles the same millisecond)
35
+
36
+ ## Install
37
+
38
+ ```console
39
+ $ gem install ruby-ulid
40
+ #=> Installed
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ The generated `ULID` is an object not just a string.
46
+ It means easily get the timestamps and binary formats.
47
+
48
+ ```ruby
49
+ require 'ulid'
50
+
51
+ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
52
+ ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
53
+ ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
54
+ ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
55
+ ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
56
+ ```
57
+
58
+ Generator can take `Time` instance
59
+
60
+ ```ruby
61
+ time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
62
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
63
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
64
+
65
+ ulids = 1000.times.map do
66
+ ULID.generate(moment: time)
67
+ end
68
+ ulids.sort == ulids #=> false
69
+
70
+ ulids = 1000.times.map do |n|
71
+ ULID.generate(moment: time + n)
72
+ end
73
+ ulids.sort == ulids #=> true
74
+ ```
75
+
76
+ You can parse from exists IDs
77
+
78
+ FYI: Current parser/validator/matcher implementation aims `strict`, It might be changed in [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57).
79
+
80
+ ```ruby
81
+ ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
82
+ ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
83
+ ```
84
+
85
+ ULIDs are sortable when they are generated in different timestamp with milliseconds precision
86
+
87
+ ```ruby
88
+ ulids = 1000.times.map do
89
+ sleep(0.001)
90
+ ULID.generate
91
+ end
92
+ ulids.sort == ulids #=> true
93
+ ulids.uniq(&:to_time).size #=> 1000
94
+ ```
95
+
96
+ The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
97
+
98
+ ```ruby
99
+ ulids = 10000.times.map do
100
+ ULID.generate
101
+ end
102
+ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in environment)
103
+ ulids.sort == ulids #=> false
104
+ ```
105
+
106
+ If you want to prefer `sortable` rather than the `strict randomness`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
107
+ (Though it starts with new random value when changed the timestamp)
108
+
109
+ ```ruby
110
+ monotonic_generator = ULID::MonotonicGenerator.new
111
+ monotonic_ulids = 10000.times.map do
112
+ monotonic_generator.generate
113
+ end
114
+ sample_ulids_by_the_time = monotonic_ulids.uniq(&:to_time)
115
+ sample_ulids_by_the_time.size #=> 34 (the size is not fixed, might be changed in environment)
116
+ sample_ulids_by_the_time.take(10).map(&:randomness)
117
+ #=>
118
+ ["JZW56CTA8704D5AQ",
119
+ "JGEBH2A2B2EA97MW",
120
+ "0XPE4NS3MZH0NAJ4",
121
+ "E0S3ZAVADFBPW57Y",
122
+ "E5CX1T6281443THQ",
123
+ "3SK8WHSH03CVF7J2",
124
+ "DDS35BT0R20P3V49",
125
+ "60KG2W9FVEN1ZX8C",
126
+ "X59YJVXXVH7AXJJE",
127
+ "1ZBQ7SNGFKXGH1Y4"]
128
+
129
+ monotonic_ulids.sort == monotonic_ulids #=> true
130
+ ```
131
+
132
+ For rough operations, `ULID.scan` might be useful.
133
+
134
+ ```ruby
135
+ json =<<'EOD'
136
+ {
137
+ "id": "01F4GNAV5ZR6FJQ5SFQC7WDSY3",
138
+ "author": {
139
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
140
+ "name": "kachick"
141
+ },
142
+ "title": "My awesome blog post",
143
+ "comments": [
144
+ {
145
+ "id": "01F4GNCNC3CH0BCRZBPPDEKBKS",
146
+ "commenter": {
147
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
148
+ "name": "kachick"
149
+ }
150
+ },
151
+ {
152
+ "id": "01F4GNCXAMXQ1SGBH5XCR6ZH0M",
153
+ "commenter": {
154
+ "id": "01F4GND4RYYSKNAADHQ9BNXAWJ",
155
+ "name": "pankona"
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ EOD
161
+
162
+ ULID.scan(json).to_a
163
+ #=>
164
+ [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
165
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
166
+ ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
167
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
168
+ ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
169
+ ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
170
+ ```
171
+
172
+ `ULID.min` and `ULID.max` return termination values for ULID spec.
173
+
174
+ ```ruby
175
+ ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
176
+ ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
177
+
178
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
179
+ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
180
+ ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
181
+ ```
182
+
183
+ `ULID#next` and `ULID#succ` returns next(successor) ULID
184
+
185
+ ```ruby
186
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
187
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM0000000000000000"
188
+ ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil
189
+ ```
190
+
191
+ `ULID#pred` returns predecessor ULID
192
+
193
+ ```ruby
194
+ ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000"
195
+ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZZZ"
196
+ ULID.parse('00000000000000000000000000').pred #=> nil
197
+ ```
198
+
199
+ UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
200
+
201
+ ```ruby
202
+ ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
203
+ #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
204
+ ```
205
+
206
+ ## References
207
+
208
+ - [API documents](https://kachick.github.io/ruby-ulid/)
209
+ - [ulid/spec](https://github.com/ulid/spec)
210
+ - [Another choices are UUIDv6, UUIDv7, UUIDv8. But they are still in draft state](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html)
data/Steepfile ADDED
@@ -0,0 +1,7 @@
1
+ target :lib do
2
+ signature 'sig'
3
+
4
+ check 'lib'
5
+
6
+ library 'securerandom'
7
+ end
data/lib/ulid.rb CHANGED
@@ -3,11 +3,13 @@
3
3
  # Copyright (C) 2021 Kenichi Kamiya
4
4
 
5
5
  require 'securerandom'
6
- require 'singleton'
7
6
  require 'integer/base'
8
- require_relative 'ulid/version'
9
7
 
10
8
  # @see https://github.com/ulid/spec
9
+ # @!attribute [r] milliseconds
10
+ # @return [Integer]
11
+ # @!attribute [r] entropy
12
+ # @return [Integer]
11
13
  class ULID
12
14
  include Comparable
13
15
 
@@ -15,77 +17,109 @@ class ULID
15
17
  class OverflowError < Error; end
16
18
  class ParserError < Error; end
17
19
 
20
+ encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
18
21
  # Crockford's Base32. Excluded I, L, O, U.
19
22
  # @see https://www.crockford.com/base32.html
20
- ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.chars.map(&:freeze).freeze
23
+ ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
21
24
 
22
- TIME_PART_LENGTH = 10
25
+ TIMESTAMP_PART_LENGTH = 10
23
26
  RANDOMNESS_PART_LENGTH = 16
24
- ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
25
- TIME_OCTETS_LENGTH = 6
27
+ ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
28
+ TIMESTAMP_OCTETS_LENGTH = 6
26
29
  RANDOMNESS_OCTETS_LENGTH = 10
27
- OCTETS_LENGTH = TIME_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
30
+ OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
28
31
  MAX_MILLISECONDS = 281474976710655
29
32
  MAX_ENTROPY = 1208925819614629174706175
33
+ MAX_INTEGER = 340282366920938463463374607431768211455
34
+ PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
35
+ STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
+
37
+ # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
38
+ UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i
30
39
 
31
40
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
32
41
  # @see https://bugs.ruby-lang.org/issues/15958
33
42
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
34
43
 
35
- class MonotonicGenerator
36
- include Singleton
37
-
38
- attr_accessor :latest_milliseconds, :latest_entropy
44
+ # @param [Integer, Time] moment
45
+ # @param [Integer] entropy
46
+ # @return [ULID]
47
+ def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
48
+ milliseconds = moment.kind_of?(Time) ? time_to_milliseconds(moment) : moment
49
+ new milliseconds: milliseconds, entropy: entropy
50
+ end
39
51
 
40
- def initialize
41
- reset
42
- end
52
+ # @param [Integer, Time] moment
53
+ # @return [ULID]
54
+ def self.min(moment: 0)
55
+ generate(moment: moment, entropy: 0)
56
+ end
43
57
 
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
58
+ # @param [Integer, Time] moment
59
+ # @return [ULID]
60
+ def self.max(moment: MAX_MILLISECONDS)
61
+ generate(moment: moment, entropy: MAX_ENTROPY)
62
+ end
57
63
 
58
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
64
+ # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
65
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
66
+ # @return [ULID]
67
+ def self.monotonic_generate
68
+ warning = "`ULID.monotonic_generate` actually changes class state. Use `ULID::MonotonicGenerator` instead."
69
+ if RUBY_VERSION >= '3.0'
70
+ Warning.warn(warning, category: :deprecated)
71
+ else
72
+ Warning.warn(warning)
59
73
  end
60
74
 
61
- # @return [self]
62
- def reset
63
- @latest_milliseconds = nil
64
- @latest_entropy = nil
65
- self
66
- end
75
+ MONOTONIC_GENERATOR.generate
76
+ end
67
77
 
68
- # @return [void]
69
- def freeze
70
- raise TypeError, "cannot freeze #{self.class}"
78
+ # @param [String, #to_str] string
79
+ # @return [Enumerator]
80
+ # @yieldparam [ULID] ulid
81
+ # @yieldreturn [self]
82
+ def self.scan(string)
83
+ string = string.to_str
84
+ return to_enum(__callee__, string) unless block_given?
85
+ string.scan(PATTERN) do |pair|
86
+ yield parse(pair.join)
71
87
  end
88
+ self
72
89
  end
73
90
 
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
91
+ # @param [String, #to_str] uuid
80
92
  # @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
93
+ # @raise [ParserError] if the given format is not correct for UUIDv4 specs
94
+ def self.from_uuidv4(uuid)
95
+ begin
96
+ uuid = uuid.to_str
97
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
98
+ raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
99
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
100
+ from_integer(normalized.to_i(16))
101
+ rescue => err
102
+ raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
103
+ end
84
104
  end
85
105
 
106
+ # @param [Integer, #to_int] integer
86
107
  # @return [ULID]
87
- def self.monotonic_generate
88
- MONOTONIC_GENERATOR.generate
108
+ # @raise [OverflowError] if the given integer is larger than the ULID limit
109
+ # @raise [ArgumentError] if the given integer is negative number
110
+ # @todo Need optimized for performance
111
+ def self.from_integer(integer)
112
+ integer = integer.to_int
113
+ raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
114
+ raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
115
+
116
+ octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
117
+ time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
118
+ randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
119
+ milliseconds = inverse_of_digits(time_octets)
120
+ entropy = inverse_of_digits(randomness_octets)
121
+
122
+ new milliseconds: milliseconds, entropy: entropy
89
123
  end
90
124
 
91
125
  # @return [Integer]
@@ -106,14 +140,16 @@ class ULID
106
140
 
107
141
  # @param [String, #to_str] string
108
142
  # @return [ULID]
143
+ # @raise [ParserError] if the given format is not correct for ULID specs
144
+ # @raise [OverflowError] if the given value is larger than the ULID limit
109
145
  def self.parse(string)
110
146
  begin
111
147
  string = string.to_str
112
148
  unless string.size == ENCODED_ID_LENGTH
113
149
  raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
114
150
  end
115
- timestamp = string.slice(0, TIME_PART_LENGTH)
116
- randomness = string.slice(TIME_PART_LENGTH, RANDOMNESS_PART_LENGTH)
151
+ timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
152
+ randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
117
153
  milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
118
154
  entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
119
155
  rescue => err
@@ -123,7 +159,6 @@ class ULID
123
159
  new milliseconds: milliseconds, entropy: entropy
124
160
  end
125
161
 
126
- # @param [String] string
127
162
  # @return [Boolean]
128
163
  def self.valid?(string)
129
164
  parse(string)
@@ -133,8 +168,36 @@ class ULID
133
168
  true
134
169
  end
135
170
 
171
+ # @param [Integer] integer
172
+ # @param [Integer] length
173
+ # @return [Array<Integer>]
174
+ def self.octets_from_integer(integer, length:)
175
+ digits = integer.digits(256)
176
+ (length - digits.size).times do
177
+ digits.push 0
178
+ end
179
+ digits.reverse!
180
+ end
181
+
182
+ # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
183
+ # @param [Array<Integer>] reversed_digits
184
+ # @return [Integer]
185
+ def self.inverse_of_digits(reversed_digits)
186
+ base = 256
187
+ num = 0
188
+ reversed_digits.each do |digit|
189
+ num = (num * base) + digit
190
+ end
191
+ num
192
+ end
193
+
136
194
  attr_reader :milliseconds, :entropy
137
195
 
196
+ # @param [Integer] milliseconds
197
+ # @param [Integer] entropy
198
+ # @return [void]
199
+ # @raise [OverflowError] if the given value is larger than the ULID limit
200
+ # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
138
201
  def initialize(milliseconds:, entropy:)
139
202
  milliseconds = milliseconds.to_int
140
203
  entropy = entropy.to_int
@@ -154,13 +217,13 @@ class ULID
154
217
 
155
218
  # @return [Integer]
156
219
  def to_i
157
- @integer ||= inverse_of_digits(octets)
220
+ @integer ||= self.class.inverse_of_digits(octets)
158
221
  end
159
222
  alias_method :hash, :to_i
160
223
 
161
224
  # @return [Integer, nil]
162
225
  def <=>(other)
163
- other.kind_of?(self.class) ? (to_i <=> other.to_i) : nil
226
+ other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
164
227
  end
165
228
 
166
229
  # @return [String]
@@ -170,68 +233,106 @@ class ULID
170
233
 
171
234
  # @return [Boolean]
172
235
  def eql?(other)
173
- other.equal?(self) || (other.kind_of?(self.class) && other.to_i == to_i)
236
+ other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
174
237
  end
175
238
  alias_method :==, :eql?
176
239
 
240
+ # @return [Boolean]
241
+ def ===(other)
242
+ case other
243
+ when ULID
244
+ self == other
245
+ when String
246
+ begin
247
+ self == self.class.parse(other)
248
+ rescue Exception
249
+ false
250
+ end
251
+ else
252
+ false
253
+ end
254
+ end
255
+
177
256
  # @return [Time]
178
257
  def to_time
179
- @time ||= Time.at(0, @milliseconds, :millisecond).utc
258
+ @time ||= Time.at(0, @milliseconds, :millisecond).utc.freeze
180
259
  end
181
260
 
182
- # @return [Array<Integer>]
261
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
183
262
  def octets
184
- @octets ||= (time_octets + randomness_octets).freeze
263
+ @octets ||= (timestamp_octets + randomness_octets).freeze
185
264
  end
186
265
 
187
- # @return [Array<Integer>]
188
- def time_octets
189
- @time_octets ||= octets_from_integer(@milliseconds, length: TIME_OCTETS_LENGTH).freeze
266
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
267
+ def timestamp_octets
268
+ @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
190
269
  end
191
270
 
192
- # @return [Array<Integer>]
271
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
193
272
  def randomness_octets
194
- @randomness_octets ||= octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
273
+ @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
195
274
  end
196
275
 
197
- # @return [ULID]
276
+ # @return [String]
277
+ def timestamp
278
+ @timestamp ||= matchdata[:timestamp].freeze
279
+ end
280
+
281
+ # @return [String]
282
+ def randomness
283
+ @randomness ||= matchdata[:randomness].freeze
284
+ end
285
+
286
+ # @return [Regexp]
287
+ def pattern
288
+ @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
289
+ end
290
+
291
+ # @return [Regexp]
292
+ def strict_pattern
293
+ @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
294
+ end
295
+
296
+ # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
198
297
  def next
199
- @next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
298
+ next_int = to_i.next
299
+ return nil if next_int > MAX_INTEGER
300
+ @next ||= self.class.from_integer(next_int)
200
301
  end
201
302
  alias_method :succ, :next
202
303
 
304
+ # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
305
+ def pred
306
+ pre_int = to_i.pred
307
+ return nil if pre_int.negative?
308
+ @pred ||= self.class.from_integer(pre_int)
309
+ end
310
+
203
311
  # @return [self]
204
312
  def freeze
205
313
  # Evaluate all caching
206
314
  inspect
207
315
  octets
208
- succ
209
316
  to_i
317
+ succ
318
+ pred
319
+ strict_pattern
210
320
  super
211
321
  end
212
322
 
213
323
  private
214
324
 
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!
325
+ # @return [MatchData]
326
+ def matchdata
327
+ @matchdata ||= STRICT_PATTERN.match(to_str).freeze
224
328
  end
329
+ end
225
330
 
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
331
+ require_relative 'ulid/version'
332
+ require_relative 'ulid/monotonic_generator'
333
+
334
+ class ULID
335
+ MONOTONIC_GENERATOR = MonotonicGenerator.new
336
+
337
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
237
338
  end
@@ -0,0 +1,44 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ class ULID
6
+ class MonotonicGenerator
7
+ attr_accessor :latest_milliseconds, :latest_entropy
8
+
9
+ def initialize
10
+ reset
11
+ end
12
+
13
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
14
+ # @return [ULID]
15
+ def generate
16
+ milliseconds = ULID.current_milliseconds
17
+ reasonable_entropy = ULID.reasonable_entropy
18
+
19
+ @latest_milliseconds ||= milliseconds
20
+ @latest_entropy ||= reasonable_entropy
21
+ if @latest_milliseconds != milliseconds
22
+ @latest_milliseconds = milliseconds
23
+ @latest_entropy = reasonable_entropy
24
+ else
25
+ @latest_entropy += 1
26
+ end
27
+
28
+ ULID.new milliseconds: milliseconds, entropy: @latest_entropy
29
+ end
30
+
31
+ # @return [self]
32
+ def reset
33
+ @latest_milliseconds = nil
34
+ @latest_entropy = nil
35
+ self
36
+ end
37
+
38
+ # @raise [TypeError] always raises exception and does not freeze self
39
+ # @return [void]
40
+ def freeze
41
+ raise TypeError, "cannot freeze #{self.class}"
42
+ end
43
+ end
44
+ end
data/lib/ulid/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  class ULID
5
- VERSION = '0.0.6'
5
+ VERSION = '0.0.11'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -2,55 +2,21 @@
2
2
  class ULID
3
3
  VERSION: String
4
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
5
+ TIMESTAMP_PART_LENGTH: 10
6
+ RANDOMNESS_PART_LENGTH: 16
7
+ ENCODED_ID_LENGTH: 26
8
+ TIMESTAMP_OCTETS_LENGTH: 6
9
+ RANDOMNESS_OCTETS_LENGTH: 10
10
+ OCTETS_LENGTH: 16
11
+ MAX_MILLISECONDS: 281474976710655
12
+ MAX_ENTROPY: 1208925819614629174706175
13
+ MAX_INTEGER: 340282366920938463463374607431768211455
14
+ TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
15
+ PATTERN: Regexp
16
+ STRICT_PATTERN: Regexp
17
+ UUIDV4_PATTERN: Regexp
14
18
  MONOTONIC_GENERATOR: MonotonicGenerator
15
19
  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
20
 
55
21
  class Error < StandardError
56
22
  end
@@ -62,8 +28,6 @@ class ULID
62
28
  end
63
29
 
64
30
  class MonotonicGenerator
65
- include Singleton
66
-
67
31
  attr_accessor latest_milliseconds: Integer?
68
32
  attr_accessor latest_entropy: Integer?
69
33
  def initialize: -> void
@@ -71,4 +35,69 @@ class ULID
71
35
  def reset: -> void
72
36
  def freeze: -> void
73
37
  end
38
+
39
+ type moment = Time | Integer
40
+ type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
41
+ type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
42
+ type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
43
+
44
+ @milliseconds: Integer
45
+ @entropy: Integer
46
+ @string: String?
47
+ @integer: Integer?
48
+ @octets: octets?
49
+ @timestamp_octets: timestamp_octets?
50
+ @randomness_octets: randomness_octets?
51
+ @timestamp: String?
52
+ @randomness: String?
53
+ @inspect: String?
54
+ @time: Time?
55
+ @next: ULID?
56
+ @pattern: Regexp?
57
+ @strict_pattern: Regexp?
58
+ @matchdata: MatchData?
59
+
60
+ def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
61
+ def self.monotonic_generate: -> ULID
62
+ def self.current_milliseconds: -> Integer
63
+ def self.time_to_milliseconds: (Time time) -> Integer
64
+ def self.reasonable_entropy: -> Integer
65
+ def self.parse: (String string) -> ULID
66
+ def self.from_uuidv4: (String uuid) -> ULID
67
+ def self.from_integer: (Integer integer) -> ULID
68
+ def self.min: (?moment: moment) -> ULID
69
+ def self.max: (?moment: moment) -> ULID
70
+ def self.valid?: (untyped string) -> bool
71
+ def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
72
+ | (String string) { (ULID ulid) -> void } -> singleton(ULID)
73
+ def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
74
+ def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
75
+ attr_reader milliseconds: Integer
76
+ attr_reader entropy: Integer
77
+ def initialize: (milliseconds: Integer, entropy: Integer) -> void
78
+ def to_str: -> String
79
+ alias to_s to_str
80
+ def to_i: -> Integer
81
+ alias hash to_i
82
+ def <=>: (ULID other) -> Integer
83
+ | (untyped other) -> Integer?
84
+ def inspect: -> String
85
+ def eql?: (untyped other) -> bool
86
+ alias == eql?
87
+ def ===: (untyped other) -> bool
88
+ def to_time: -> Time
89
+ def timestamp: -> String
90
+ def randomness: -> String
91
+ def pattern: -> Regexp
92
+ def strict_pattern: -> Regexp
93
+ def octets: -> octets
94
+ def timestamp_octets: -> timestamp_octets
95
+ def randomness_octets: -> randomness_octets
96
+ def next: -> ULID?
97
+ alias succ next
98
+ def pred: -> ULID?
99
+ def freeze: -> self
100
+
101
+ private
102
+ def matchdata: -> MatchData
74
103
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-ulid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenichi Kamiya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-29 00:00:00.000000000 Z
11
+ date: 2021-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base
@@ -17,33 +17,19 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
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
20
  - - "<"
35
21
  - !ruby/object:Gem::Version
36
- version: '4'
37
- type: :development
22
+ version: 0.2.0
23
+ type: :runtime
38
24
  prerelease: false
39
25
  version_requirements: !ruby/object:Gem::Requirement
40
26
  requirements:
41
27
  - - ">="
42
28
  - !ruby/object:Gem::Version
43
- version: 3.4.1
29
+ version: 0.1.2
44
30
  - - "<"
45
31
  - !ruby/object:Gem::Version
46
- version: '4'
32
+ version: 0.2.0
47
33
  - !ruby/object:Gem::Dependency
48
34
  name: benchmark-ips
49
35
  requirement: !ruby/object:Gem::Requirement
@@ -84,18 +70,21 @@ dependencies:
84
70
  - - "<"
85
71
  - !ruby/object:Gem::Version
86
72
  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.
73
+ description: " ULID(Universally Unique Lexicographically Sortable Identifier) has
74
+ useful specs for applications (e.g. `Database key`). \n This gem aims to provide
75
+ the generator, monotonic generator, parser and handy manipulation features around
76
+ the ULID.\n Also providing `rbs` signature files.\n"
92
77
  email:
93
78
  - kachick1+ruby@gmail.com
94
79
  executables: []
95
80
  extensions: []
96
81
  extra_rdoc_files: []
97
82
  files:
83
+ - LICENSE
84
+ - README.md
85
+ - Steepfile
98
86
  - lib/ulid.rb
87
+ - lib/ulid/monotonic_generator.rb
99
88
  - lib/ulid/version.rb
100
89
  - sig/ulid.rbs
101
90
  homepage: https://github.com/kachick/ruby-ulid