ruby-ulid 0.0.11 → 0.0.16

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: 91f6d4c9dad8e12663686099c487dbbf3ec5b7995e0e4b2b210ac8a16fbcd91c
4
- data.tar.gz: 1ca6e29d83771636b2a20aa71d90f15699d07d6a7a75f22c306094507bb51d89
3
+ metadata.gz: 15a83604732cdc37f8015ee9382884e950852f8c9c82aba107ea4b3c065c5a4d
4
+ data.tar.gz: 06e7b69a5838786f4d23da19f5e1f00fb3173014e7438459165b66703637851a
5
5
  SHA512:
6
- metadata.gz: 22dcc2541f7e4fa1a1d4014f996aef3d3bfcb69b002c1cc6ca4864e17bce50a327e1ec98453b4dab0773400b678055dc024a7287342a08916ce30772aadb418d
7
- data.tar.gz: 27c69c7319858a4c732f04f6944ff180f3dfc9a5df12bd67cd1e4cac91421a2bc00d1a79c8ff91190b425731f831fdaef1a2f5bc0c3ef4fdaff73a6ce6749e35
6
+ metadata.gz: 6ca46525e80b1d832a703b8cb867466327de333853ee236654f722dc6abc30f46fd149ca9633c42ececcea08db2a01a6d82ab60f8eeb61ebb947f0441436dd96
7
+ data.tar.gz: ccd08e78dfb047f82af02eeaf14ccd12fcb05fe882edeb2bf48c2e7ec5b9fd698feccf0b9f9e187aaff97c2e793f671a3f9d32c11e3741656c3df94f944bc808
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # ruby-ulid
2
2
 
3
- A handy `ULID` library
3
+ ## Overview
4
4
 
5
- The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
5
+ The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec). It has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
6
6
  This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
7
- Also providing rbs signature files.
7
+ Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
8
8
 
9
9
  ---
10
10
 
@@ -28,19 +28,31 @@ Instead, herein is proposed ULID:
28
28
  - 1.21e+24 unique ULIDs per millisecond
29
29
  - Lexicographically sortable!
30
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)
31
+ - Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character) # See also exists issues in [Note](#note)
32
32
  - Case insensitive
33
33
  - No special characters (URL safe)
34
34
  - Monotonic sort order (correctly detects and handles the same millisecond)
35
35
 
36
- ## Install
36
+ ## Usage
37
+
38
+ ### Install
39
+
40
+ Require Ruby 2.6 or later
41
+
42
+ This command will install the latest version into your environment
37
43
 
38
44
  ```console
39
45
  $ gem install ruby-ulid
40
- #=> Installed
46
+ Should be installed!
41
47
  ```
42
48
 
43
- ## Usage
49
+ Add this line to your application/library's `Gemfile` is needed in basic use-case
50
+
51
+ ```ruby
52
+ gem 'ruby-ulid', '0.0.16'
53
+ ```
54
+
55
+ ### Generator and Parser
44
56
 
45
57
  The generated `ULID` is an object not just a string.
46
58
  It means easily get the timestamps and binary formats.
@@ -52,36 +64,17 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
52
64
  ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
53
65
  ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
54
66
  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
67
  ```
75
68
 
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).
69
+ You can get the objects from exists encoded ULIDs
79
70
 
80
71
  ```ruby
81
72
  ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
82
73
  ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
83
74
  ```
84
75
 
76
+ ### Sortable with the timestamp
77
+
85
78
  ULIDs are sortable when they are generated in different timestamp with milliseconds precision
86
79
 
87
80
  ```ruby
@@ -89,8 +82,21 @@ ulids = 1000.times.map do
89
82
  sleep(0.001)
90
83
  ULID.generate
91
84
  end
92
- ulids.sort == ulids #=> true
93
85
  ulids.uniq(&:to_time).size #=> 1000
86
+ ulids.sort == ulids #=> true
87
+ ```
88
+
89
+ `ULID.generate` can take fixed `Time` instance
90
+
91
+ ```ruby
92
+ time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
93
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
94
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
95
+
96
+ ulids = 1000.times.map do |n|
97
+ ULID.generate(moment: time + n)
98
+ end
99
+ ulids.sort == ulids #=> true
94
100
  ```
95
101
 
96
102
  The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
@@ -103,32 +109,72 @@ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in en
103
109
  ulids.sort == ulids #=> false
104
110
  ```
105
111
 
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.
112
+ ### How to keep `Sortable` even if in same timestamp
113
+
114
+ If you want to prefer `sortable`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
107
115
  (Though it starts with new random value when changed the timestamp)
108
116
 
109
117
  ```ruby
110
118
  monotonic_generator = ULID::MonotonicGenerator.new
111
- monotonic_ulids = 10000.times.map do
119
+ ulids = 10000.times.map do
112
120
  monotonic_generator.generate
113
121
  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"]
122
+ sample_ulids_by_the_time = ulids.uniq(&:to_time)
123
+ sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
124
+
125
+ # In same milliseconds creation, it just increments the end of randomness part
126
+ ulids.take(5) #=>
127
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
128
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
129
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6),
130
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
131
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
132
+
133
+ # When the milliseconds is updated, it starts with new randomness
134
+ sample_ulids_by_the_time.take(5) #=>
135
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
136
+ # ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
137
+ # ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K),
138
+ # ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
139
+ # ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
140
+
141
+ ulids.sort == ulids #=> true
142
+ ```
143
+
144
+ ### Filtering IDs with `Time`
145
+
146
+ `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
147
+
148
+ ```ruby
149
+ include_end = ulid1..ulid2
150
+ exclude_end = ulid1...ulid2
151
+
152
+ ulids.grep(one_of_the_above)
153
+ ulids.grep_v(one_of_the_above)
154
+ ```
155
+
156
+ When want to filter ULIDs with `Time`, we should consider to handle the precision.
157
+ So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
128
158
 
129
- monotonic_ulids.sort == monotonic_ulids #=> true
159
+ ```ruby
160
+ # Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
161
+ include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
162
+ exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
163
+
164
+ # Below patterns are acceptable
165
+ pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
166
+ until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
167
+ until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
168
+ until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
169
+
170
+ # So you can use the generated range objects as below
171
+ ulids.grep(one_of_the_above)
172
+ ulids.grep_v(one_of_the_above)
173
+ #=> I hope the results should be actually you want!
130
174
  ```
131
175
 
176
+ ### Scanner for string (e.g. `JSON`)
177
+
132
178
  For rough operations, `ULID.scan` might be useful.
133
179
 
134
180
  ```ruby
@@ -169,6 +215,8 @@ ULID.scan(json).to_a
169
215
  ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
170
216
  ```
171
217
 
218
+ ### Some methods to help manipulations
219
+
172
220
  `ULID.min` and `ULID.max` return termination values for ULID spec.
173
221
 
174
222
  ```ruby
@@ -180,7 +228,10 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
180
228
  ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
181
229
  ```
182
230
 
183
- `ULID#next` and `ULID#succ` returns next(successor) ULID
231
+ `ULID#next` and `ULID#succ` returns next(successor) ULID.
232
+ Especially `ULID#succ` makes it possible `Range[ULID]#each`.
233
+
234
+ NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
184
235
 
185
236
  ```ruby
186
237
  ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
@@ -196,15 +247,87 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
196
247
  ULID.parse('00000000000000000000000000').pred #=> nil
197
248
  ```
198
249
 
199
- UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
250
+ ### UUIDv4 converter for migration use-cases
251
+
252
+ `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
253
+ The imported timestamp is meaningless. So ULID's benefit will lost
200
254
 
201
255
  ```ruby
202
- ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
203
- #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
256
+ # Basically reversible
257
+ ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
258
+ ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
259
+
260
+ uuid_v4s = 10000.times.map { SecureRandom.uuid }
261
+ uuid_v4s.uniq.size == 10000 #=> Probably `true`
262
+
263
+ ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
264
+ ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
265
+
266
+ # NOTE: Some boundary values are not reversible. See below.
267
+
268
+ ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
269
+ ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
270
+
271
+ # These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
272
+ reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
273
+ reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
274
+
275
+ # But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
276
+ ULID.min == reversed_min #=> false
277
+ ULID.max == reversed_max #=> false
278
+ ```
279
+
280
+ ## How to migrate from other gems
281
+
282
+ As far as I know, major prior arts are below
283
+
284
+ ### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
285
+
286
+ It is just providing basic `String` generator only.
287
+ So you can replace the code as below
288
+
289
+ ```diff
290
+ -ULID.generate
291
+ +ULID.generate.to_s
204
292
  ```
205
293
 
294
+ NOTE: It had crucial issue for handling precision, in version before `1.3.0`, when you extract timestamps from old generated ULIDs, it might be not accurate value.
295
+
296
+ 1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
297
+ 1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
298
+ 1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
299
+
300
+ ### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
301
+
302
+ It is providing basic generator(except monotonic generator) and parser.
303
+ Major methods can be replaced as below.
304
+
305
+ ```diff
306
+ -ULID.generate
307
+ +ULID.generate.to_s
308
+ -ULID.at(time)
309
+ +ULID.generate(moment: time).to_s
310
+ -ULID.time(string)
311
+ +ULID.parse(string).to_time
312
+ -ULID.min_ulid_at(time)
313
+ +ULID.min(moment: time).to_s
314
+ -ULID.max_ulid_at(time)
315
+ +ULID.max(moment: time).to_s
316
+ ```
317
+
318
+ NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
319
+
320
+ 1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
321
+ 1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
322
+ 1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
323
+
206
324
  ## References
207
325
 
326
+ - [Repository](https://github.com/kachick/ruby-ulid)
208
327
  - [API documents](https://kachick.github.io/ruby-ulid/)
209
328
  - [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)
329
+
330
+ ## Note
331
+
332
+ - Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (But they are still in draft state)](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
333
+ - Current parser/validator/matcher aims to cover `subset of Crockford's base32`. Suggesting it in [ulid/spec#57](https://github.com/ulid/spec/pull/57). Be that as it may, I might provide special handler or converter for the exception in [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57) and/or [ruby-ulid#78](https://github.com/kachick/ruby-ulid/issues/78)
data/lib/ulid.rb CHANGED
@@ -16,6 +16,7 @@ class ULID
16
16
  class Error < StandardError; end
17
17
  class OverflowError < Error; end
18
18
  class ParserError < Error; end
19
+ class SetupError < ScriptError; end
19
20
 
20
21
  encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
22
  # Crockford's Base32. Excluded I, L, O, U.
@@ -35,7 +36,7 @@ class ULID
35
36
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
37
 
37
38
  # 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
39
+ 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.freeze
39
40
 
40
41
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
42
  # @see https://bugs.ruby-lang.org/issues/15958
@@ -45,20 +46,19 @@ class ULID
45
46
  # @param [Integer] entropy
46
47
  # @return [ULID]
47
48
  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
49
+ new milliseconds: milliseconds_from_moment(moment), entropy: entropy
50
50
  end
51
51
 
52
52
  # @param [Integer, Time] moment
53
53
  # @return [ULID]
54
54
  def self.min(moment: 0)
55
- generate(moment: moment, entropy: 0)
55
+ 0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
56
56
  end
57
57
 
58
58
  # @param [Integer, Time] moment
59
59
  # @return [ULID]
60
60
  def self.max(moment: MAX_MILLISECONDS)
61
- generate(moment: moment, entropy: MAX_ENTROPY)
61
+ MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
62
62
  end
63
63
 
64
64
  # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
@@ -122,27 +122,82 @@ class ULID
122
122
  new milliseconds: milliseconds, entropy: entropy
123
123
  end
124
124
 
125
+ # @param [Range<Time>, Range<nil>] time_range
126
+ # @return [Range<ULID>]
127
+ # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
128
+ def self.range(time_range)
129
+ raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
130
+ begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
131
+
132
+ case begin_time
133
+ when Time
134
+ begin_ulid = min(moment: begin_time)
135
+ when nil
136
+ begin_ulid = min
137
+ else
138
+ raise argument_error_for_range_building(time_range)
139
+ end
140
+
141
+ case end_time
142
+ when Time
143
+ if exclude_end
144
+ end_ulid = min(moment: end_time)
145
+ else
146
+ end_ulid = max(moment: end_time)
147
+ end
148
+ when nil
149
+ # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
150
+ end_ulid = max
151
+ exclude_end = false
152
+ else
153
+ raise argument_error_for_range_building(time_range)
154
+ end
155
+
156
+ begin_ulid.freeze
157
+ end_ulid.freeze
158
+
159
+ Range.new(begin_ulid, end_ulid, exclude_end)
160
+ end
161
+
162
+ # @param [Time] time
163
+ # @return [Time]
164
+ def self.floor(time)
165
+ if RUBY_VERSION >= '2.7'
166
+ time.floor(3)
167
+ else
168
+ Time.at(0, milliseconds_from_time(time), :millisecond)
169
+ end
170
+ end
171
+
125
172
  # @return [Integer]
126
173
  def self.current_milliseconds
127
- time_to_milliseconds(Time.now)
174
+ milliseconds_from_time(Time.now)
128
175
  end
129
176
 
130
177
  # @param [Time] time
131
178
  # @return [Integer]
132
- def self.time_to_milliseconds(time)
179
+ def self.milliseconds_from_time(time)
133
180
  (time.to_r * 1000).to_i
134
181
  end
135
182
 
183
+ # @param [Time, Integer] moment
184
+ # @return [Integer]
185
+ def self.milliseconds_from_moment(moment)
186
+ moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
187
+ end
188
+
136
189
  # @return [Integer]
137
190
  def self.reasonable_entropy
138
191
  SecureRandom.random_number(MAX_ENTROPY)
139
192
  end
140
193
 
194
+ # @api private
195
+ # @deprecated Just exists to compare performance with old implementation. ref: https://github.com/kachick/ruby-ulid/issues/7
141
196
  # @param [String, #to_str] string
142
197
  # @return [ULID]
143
198
  # @raise [ParserError] if the given format is not correct for ULID specs
144
199
  # @raise [OverflowError] if the given value is larger than the ULID limit
145
- def self.parse(string)
200
+ def self.parse_with_integer_base(string)
146
201
  begin
147
202
  string = string.to_str
148
203
  unless string.size == ENCODED_ID_LENGTH
@@ -159,6 +214,67 @@ class ULID
159
214
  new milliseconds: milliseconds, entropy: entropy
160
215
  end
161
216
 
217
+ n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
218
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
219
+
220
+ n32_char_by_number = {}
221
+ n32_chars.each_with_index do |char, index|
222
+ n32_char_by_number[index] = char
223
+ end
224
+ n32_char_by_number.freeze
225
+
226
+ # Currently supporting only for `subset for actual use-case`
227
+ # See below
228
+ # * https://github.com/ulid/spec/pull/57
229
+ # * https://github.com/kachick/ruby-ulid/issues/57
230
+ # * https://github.com/kachick/ruby-ulid/issues/78
231
+ crockford_base32_mappings = {
232
+ 'J' => 18,
233
+ 'K' => 19,
234
+ 'M' => 20,
235
+ 'N' => 21,
236
+ 'P' => 22,
237
+ 'Q' => 23,
238
+ 'R' => 24,
239
+ 'S' => 25,
240
+ 'T' => 26,
241
+ 'V' => 27,
242
+ 'W' => 28,
243
+ 'X' => 29,
244
+ 'Y' => 30,
245
+ 'Z' => 31
246
+ }.freeze
247
+
248
+ REPLACING_MAP = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
249
+ if n = crockford_base32_mappings[encoding_char]
250
+ char_32 = n32_char_by_number.fetch(n)
251
+ map[encoding_char] = char_32
252
+ end
253
+ end.freeze
254
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless REPLACING_MAP.keys == crockford_base32_mappings.keys
255
+ REPLACING_PATTERN = /[#{REPLACING_MAP.keys.join}]/.freeze
256
+
257
+ def self.parse(string)
258
+ begin
259
+ string = string.to_str
260
+ raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
261
+ n32encoded = convert_crockford_base32_to_n32(string.upcase)
262
+ timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
263
+ randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
264
+ milliseconds = timestamp.to_i(32)
265
+ entropy = randomness.to_i(32)
266
+ rescue => err
267
+ raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
268
+ end
269
+
270
+ new milliseconds: milliseconds, entropy: entropy
271
+ end
272
+
273
+ # @api private
274
+ private_class_method def self.convert_crockford_base32_to_n32(string)
275
+ string.gsub(REPLACING_PATTERN, REPLACING_MAP)
276
+ end
277
+
162
278
  # @return [Boolean]
163
279
  def self.valid?(string)
164
280
  parse(string)
@@ -168,6 +284,7 @@ class ULID
168
284
  true
169
285
  end
170
286
 
287
+ # @api private
171
288
  # @param [Integer] integer
172
289
  # @param [Integer] length
173
290
  # @return [Array<Integer>]
@@ -179,6 +296,7 @@ class ULID
179
296
  digits.reverse!
180
297
  end
181
298
 
299
+ # @api private
182
300
  # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
183
301
  # @param [Array<Integer>] reversed_digits
184
302
  # @return [Integer]
@@ -191,8 +309,14 @@ class ULID
191
309
  num
192
310
  end
193
311
 
312
+ # @return [ArgumentError]
313
+ private_class_method def self.argument_error_for_range_building(argument)
314
+ ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
315
+ end
316
+
194
317
  attr_reader :milliseconds, :entropy
195
318
 
319
+ # @api private
196
320
  # @param [Integer] milliseconds
197
321
  # @param [Integer] entropy
198
322
  # @return [void]
@@ -210,10 +334,9 @@ class ULID
210
334
  end
211
335
 
212
336
  # @return [String]
213
- def to_str
337
+ def to_s
214
338
  @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
215
339
  end
216
- alias_method :to_s, :to_str
217
340
 
218
341
  # @return [Integer]
219
342
  def to_i
@@ -228,7 +351,7 @@ class ULID
228
351
 
229
352
  # @return [String]
230
353
  def inspect
231
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
354
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
232
355
  end
233
356
 
234
357
  # @return [Boolean]
@@ -255,7 +378,13 @@ class ULID
255
378
 
256
379
  # @return [Time]
257
380
  def to_time
258
- @time ||= Time.at(0, @milliseconds, :millisecond).utc.freeze
381
+ @time ||= begin
382
+ if RUBY_VERSION >= '2.7'
383
+ Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
384
+ else
385
+ Time.at(0, @milliseconds, :millisecond).utc.freeze
386
+ end
387
+ end
259
388
  end
260
389
 
261
390
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
@@ -283,11 +412,13 @@ class ULID
283
412
  @randomness ||= matchdata[:randomness].freeze
284
413
  end
285
414
 
415
+ # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
286
416
  # @return [Regexp]
287
417
  def pattern
288
418
  @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
289
419
  end
290
420
 
421
+ # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
291
422
  # @return [Regexp]
292
423
  def strict_pattern
293
424
  @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
@@ -308,15 +439,21 @@ class ULID
308
439
  @pred ||= self.class.from_integer(pre_int)
309
440
  end
310
441
 
442
+ # @return [String]
443
+ def to_uuidv4
444
+ @uuidv4 ||= begin
445
+ # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
446
+ array = octets.pack('C*').unpack('NnnnnN')
447
+ array[2] = (array[2] & 0x0fff) | 0x4000
448
+ array[3] = (array[3] & 0x3fff) | 0x8000
449
+ ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
450
+ end
451
+ end
452
+
311
453
  # @return [self]
312
454
  def freeze
313
- # Evaluate all caching
314
- inspect
315
- octets
316
- to_i
317
- succ
318
- pred
319
- strict_pattern
455
+ # Need to cache before freezing, because frozen objects can't assign instance variables
456
+ cache_all_instance_variables
320
457
  super
321
458
  end
322
459
 
@@ -324,7 +461,18 @@ class ULID
324
461
 
325
462
  # @return [MatchData]
326
463
  def matchdata
327
- @matchdata ||= STRICT_PATTERN.match(to_str).freeze
464
+ @matchdata ||= STRICT_PATTERN.match(to_s).freeze
465
+ end
466
+
467
+ # @return [void]
468
+ def cache_all_instance_variables
469
+ inspect
470
+ octets
471
+ to_i
472
+ succ
473
+ pred
474
+ strict_pattern
475
+ to_uuidv4
328
476
  end
329
477
  end
330
478
 
@@ -332,7 +480,9 @@ require_relative 'ulid/version'
332
480
  require_relative 'ulid/monotonic_generator'
333
481
 
334
482
  class ULID
483
+ MIN = parse('00000000000000000000000000').freeze
484
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
335
485
  MONOTONIC_GENERATOR = MonotonicGenerator.new
336
486
 
337
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
487
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX, :REPLACING_PATTERN, :REPLACING_MAP
338
488
  end
@@ -4,35 +4,37 @@
4
4
 
5
5
  class ULID
6
6
  class MonotonicGenerator
7
+ # @api private
7
8
  attr_accessor :latest_milliseconds, :latest_entropy
8
9
 
9
10
  def initialize
10
11
  reset
11
12
  end
12
13
 
13
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
14
+ # @param [Time, Integer] moment
14
15
  # @return [ULID]
15
- def generate
16
- milliseconds = ULID.current_milliseconds
17
- reasonable_entropy = ULID.reasonable_entropy
16
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
17
+ # @raise [ArgumentError] if the given moment(milliseconds) is negative number
18
+ def generate(moment: ULID.current_milliseconds)
19
+ milliseconds = ULID.milliseconds_from_moment(moment)
20
+ raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
18
21
 
19
- @latest_milliseconds ||= milliseconds
20
- @latest_entropy ||= reasonable_entropy
21
- if @latest_milliseconds != milliseconds
22
+ if @latest_milliseconds < milliseconds
22
23
  @latest_milliseconds = milliseconds
23
- @latest_entropy = reasonable_entropy
24
+ @latest_entropy = ULID.reasonable_entropy
24
25
  else
25
26
  @latest_entropy += 1
26
27
  end
27
28
 
28
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
29
+ ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
29
30
  end
30
31
 
31
- # @return [self]
32
+ # @api private
33
+ # @return [void]
32
34
  def reset
33
- @latest_milliseconds = nil
34
- @latest_entropy = nil
35
- self
35
+ @latest_milliseconds = 0
36
+ @latest_entropy = ULID.reasonable_entropy
37
+ nil
36
38
  end
37
39
 
38
40
  # @raise [TypeError] always raises exception and does not freeze self
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.11'
5
+ VERSION = '0.0.16'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -15,9 +15,16 @@ class ULID
15
15
  PATTERN: Regexp
16
16
  STRICT_PATTERN: Regexp
17
17
  UUIDV4_PATTERN: Regexp
18
+ REPLACING_MAP: Hash[String, String]
19
+ REPLACING_PATTERN: Regexp
18
20
  MONOTONIC_GENERATOR: MonotonicGenerator
21
+ MIN: ULID
22
+ MAX: ULID
19
23
  include Comparable
20
24
 
25
+ # The `moment` is a `Time` or `Intger of the milliseconds`
26
+ type moment = Time | Integer
27
+
21
28
  class Error < StandardError
22
29
  end
23
30
 
@@ -27,16 +34,18 @@ class ULID
27
34
  class ParserError < Error
28
35
  end
29
36
 
37
+ class SetupError < ScriptError
38
+ end
39
+
30
40
  class MonotonicGenerator
31
- attr_accessor latest_milliseconds: Integer?
32
- attr_accessor latest_entropy: Integer?
41
+ attr_accessor latest_milliseconds: Integer
42
+ attr_accessor latest_entropy: Integer
33
43
  def initialize: -> void
34
- def generate: -> ULID
44
+ def generate: (?moment: moment) -> ULID
35
45
  def reset: -> void
36
46
  def freeze: -> void
37
47
  end
38
48
 
39
- type moment = Time | Integer
40
49
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
41
50
  type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
42
51
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
@@ -55,12 +64,16 @@ class ULID
55
64
  @next: ULID?
56
65
  @pattern: Regexp?
57
66
  @strict_pattern: Regexp?
67
+ @uuidv4: String?
58
68
  @matchdata: MatchData?
59
69
 
60
70
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
61
71
  def self.monotonic_generate: -> ULID
62
72
  def self.current_milliseconds: -> Integer
63
- def self.time_to_milliseconds: (Time time) -> Integer
73
+ def self.milliseconds_from_time: (Time time) -> Integer
74
+ def self.milliseconds_from_moment: (moment moment) -> Integer
75
+ def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
76
+ def self.floor: (Time time) -> Time
64
77
  def self.reasonable_entropy: -> Integer
65
78
  def self.parse: (String string) -> ULID
66
79
  def self.from_uuidv4: (String uuid) -> ULID
@@ -72,11 +85,11 @@ class ULID
72
85
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
73
86
  def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
74
87
  def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
88
+ def self.convert_crockford_base32_to_n32: (String) -> String
75
89
  attr_reader milliseconds: Integer
76
90
  attr_reader entropy: Integer
77
91
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
78
- def to_str: -> String
79
- alias to_s to_str
92
+ def to_s: -> String
80
93
  def to_i: -> Integer
81
94
  alias hash to_i
82
95
  def <=>: (ULID other) -> Integer
@@ -93,11 +106,14 @@ class ULID
93
106
  def octets: -> octets
94
107
  def timestamp_octets: -> timestamp_octets
95
108
  def randomness_octets: -> randomness_octets
109
+ def to_uuidv4: -> String
96
110
  def next: -> ULID?
97
111
  alias succ next
98
112
  def pred: -> ULID?
99
113
  def freeze: -> self
100
114
 
101
115
  private
116
+ def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
102
117
  def matchdata: -> MatchData
118
+ def cache_all_instance_variables: -> void
103
119
  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.11
4
+ version: 0.0.16
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-05-01 00:00:00.000000000 Z
11
+ date: 2021-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.2.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: rbs
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.2.0
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.2.0
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: benchmark-ips
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -70,10 +84,10 @@ dependencies:
70
84
  - - "<"
71
85
  - !ruby/object:Gem::Version
72
86
  version: '2'
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"
87
+ description: |2
88
+ The ULID(Universally Unique Lexicographically Sortable Identifier) has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
89
+ This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
90
+ Also providing `ruby/rbs` signature files.
77
91
  email:
78
92
  - kachick1+ruby@gmail.com
79
93
  executables: []
@@ -82,7 +96,6 @@ extra_rdoc_files: []
82
96
  files:
83
97
  - LICENSE
84
98
  - README.md
85
- - Steepfile
86
99
  - lib/ulid.rb
87
100
  - lib/ulid/monotonic_generator.rb
88
101
  - lib/ulid/version.rb
@@ -102,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
102
115
  requirements:
103
116
  - - ">="
104
117
  - !ruby/object:Gem::Version
105
- version: '2.5'
118
+ version: 2.6.0
106
119
  required_rubygems_version: !ruby/object:Gem::Requirement
107
120
  requirements:
108
121
  - - ">="
data/Steepfile DELETED
@@ -1,7 +0,0 @@
1
- target :lib do
2
- signature 'sig'
3
-
4
- check 'lib'
5
-
6
- library 'securerandom'
7
- end