Class: ULID

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/ulid.rb,
lib/ulid.rb,
lib/ulid/uuid.rb,
lib/ulid/version.rb,
lib/ulid/crockford_base32.rb,
lib/ulid/monotonic_generator.rb

Overview

Copyright (C) 2021 Kenichi Kamiya

Defined Under Namespace

Modules: CrockfordBase32 Classes: Error, MonotonicGenerator, OverflowError, ParserError, UnexpectedError

Constant Summary collapse

CROCKFORD_BASE32_ENCODING_STRING =

Excluded I, L, O, U, -. This is the encoding patterns. The decoding issue is written in ULID::CrockfordBase32

'0123456789ABCDEFGHJKMNPQRSTVWXYZ'
TIMESTAMP_ENCODED_LENGTH =
10
RANDOMNESS_ENCODED_LENGTH =
16
ENCODED_LENGTH =
TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
TIMESTAMP_OCTETS_LENGTH =
6
RANDOMNESS_OCTETS_LENGTH =
10
OCTETS_LENGTH =
TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
MAX_MILLISECONDS =
281474976710655
MAX_ENTROPY =
1208925819614629174706175
MAX_INTEGER =
340282366920938463463374607431768211455
PATTERN_WITH_CROCKFORD_BASE32_SUBSET =

Currently not used as a constant, but kept as a reference for now.

/(?<timestamp>[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET =
/\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
SCANNING_PATTERN =

Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed. This can't contain `b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.

/[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
VERSION =
'0.1.6'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(milliseconds:, entropy:, integer:) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • milliseconds (Integer)
  • entropy (Integer)
  • integer (Integer)


361
362
363
364
365
366
# File 'lib/ulid.rb', line 361

def initialize(milliseconds:, entropy:, integer:)
  # All arguments check should be done with each constructors, not here
  @integer = integer
  @milliseconds = milliseconds
  @entropy = entropy
end

Instance Attribute Details

#entropyInteger (readonly)

Returns:

  • (Integer)


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'lib/ulid.rb', line 13

class ULID
  include Comparable

  class Error < StandardError; end
  class OverflowError < Error; end
  class ParserError < Error; end
  class UnexpectedError < Error; end

  # Excluded I, L, O, U, -.
  # This is the encoding patterns.
  # The decoding issue is written in ULID::CrockfordBase32
  CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'

  TIMESTAMP_ENCODED_LENGTH = 10
  RANDOMNESS_ENCODED_LENGTH = 16
  ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
  TIMESTAMP_OCTETS_LENGTH = 6
  RANDOMNESS_OCTETS_LENGTH = 10
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
  MAX_MILLISECONDS = 281474976710655
  MAX_ENTROPY = 1208925819614629174706175
  MAX_INTEGER = 340282366920938463463374607431768211455

  # @see https://github.com/ulid/spec/pull/57
  # Currently not used as a constant, but kept as a reference for now.
  PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze

  STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze

  # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
  # This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
  SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze

  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
  # @see https://bugs.ruby-lang.org/issues/15958
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'

  private_class_method :new

  # @param [Integer, Time] moment
  # @param [Integer] entropy
  # @return [ULID]
  def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
    from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
  end

  # Short hand of `ULID.generate(moment: time)`
  # @param [Time] time
  # @return [ULID]
  def self.at(time)
    raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time

    from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
  end

  # @param [Time, Integer] moment
  # @return [ULID]
  def self.min(moment=0)
    0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
  end

  # @param [Time, Integer] moment
  # @return [ULID]
  def self.max(moment=MAX_MILLISECONDS)
    MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
  end

  RANDOM_INTEGER_GENERATOR = -> {
    SecureRandom.random_number(MAX_INTEGER)
  }

  # @param [Range<Time>, Range<nil>, Range[ULID], nil] period
  # @overload sample(number, period: nil)
  #   @param [Integer] number
  #   @return [Array<ULID>]
  #   @raise [ArgumentError] if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number
  # @overload sample(period: nil)
  #   @return [ULID]
  # @note Major difference of `Array#sample` interface is below
  #   * Do not ensure the uniqueness
  #   * Do not take random generator for the arguments
  #   * Raising error instead of truncating elements for the given number
  def self.sample(*args, period: nil)
    int_generator = (
      if period
        ulid_range = range(period)
        min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?

        possibilities = (max - min) + (exclude_end ? 0 : 1)
        raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?

        -> {
          SecureRandom.random_number(possibilities) + min
        }
      else
        RANDOM_INTEGER_GENERATOR
      end
    )

    case args.size
    when 0
      from_integer(int_generator.call)
    when 1
      number = args.first
      raise ArgumentError, 'accepts no argument or integer only' unless Integer === number

      if number > MAX_INTEGER || number.negative?
        raise ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative"
      end

      if period && (number > possibilities)
        raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
      end

      Array.new(number) { from_integer(int_generator.call) }
    else
      raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
    end
  end

  # @param [String, #to_str] string
  # @return [Enumerator]
  # @yieldparam [ULID] ulid
  # @yieldreturn [self]
  def self.scan(string)
    string = String.try_convert(string)
    raise ArgumentError, 'ULID.scan takes only strings' unless string
    return to_enum(__callee__, string) unless block_given?

    string.scan(SCANNING_PATTERN) do |matched|
      yield parse(matched)
    end
    self
  end

  # @param [Integer] integer
  # @return [ULID]
  # @raise [OverflowError] if the given integer is larger than the ULID limit
  # @raise [ArgumentError] if the given integer is negative number
  def self.from_integer(integer)
    raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
    raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
    raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?

    n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
    n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
    n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)

    milliseconds = n32encoded_timestamp.to_i(32)
    entropy = n32encoded_randomness.to_i(32)

    new(milliseconds: milliseconds, entropy: entropy, integer: integer)
  end

  # @param [Range<Time>, Range<nil>, Range[ULID]] period
  # @return [Range<ULID>]
  # @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
  def self.range(period)
    raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period

    begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
    return period if self === begin_element && self === end_element

    case begin_element
    when Time
      begin_ulid = min(begin_element)
    when nil
      begin_ulid = MIN
    when self
      begin_ulid = begin_element
    else
      raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
    end

    case end_element
    when Time
      end_ulid = exclude_end ? min(end_element) : max(end_element)
    when nil
      # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
      end_ulid = MAX
      exclude_end = false
    when self
      end_ulid = end_element
    else
      raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
    end

    begin_ulid.freeze
    end_ulid.freeze

    Range.new(begin_ulid, end_ulid, exclude_end)
  end

  # @param [Time] time
  # @return [Time]
  def self.floor(time)
    raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time

    if RUBY_VERSION >= '2.7'
      time.floor(3)
    else
      Time.at(0, milliseconds_from_time(time), :millisecond)
    end
  end

  # @api private
  # @return [Integer]
  def self.current_milliseconds
    milliseconds_from_time(Time.now)
  end

  # @api private
  # @param [Time] time
  # @return [Integer]
  private_class_method def self.milliseconds_from_time(time)
    (time.to_r * 1000).to_i
  end

  # @api private
  # @param [Time, Integer] moment
  # @return [Integer]
  def self.milliseconds_from_moment(moment)
    case moment
    when Integer
      moment
    when Time
      milliseconds_from_time(moment)
    else
      raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
    end
  end

  # @return [Integer]
  private_class_method def self.reasonable_entropy
    SecureRandom.random_number(MAX_ENTROPY)
  end

  # @param [String, #to_str] string
  # @return [ULID]
  # @raise [ParserError] if the given format is not correct for ULID specs
  def self.parse(string)
    string = String.try_convert(string)
    raise ArgumentError, 'ULID.parse takes only strings' unless string

    unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
      raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
    end

    from_integer(CrockfordBase32.decode(string))
  end

  # @param [String, #to_str] string
  # @return [String]
  # @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
  def self.normalize(string)
    string = String.try_convert(string)
    raise ArgumentError, 'ULID.normalize takes only strings' unless string

    normalized_in_crockford = CrockfordBase32.normalize(string)
    # Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
    parse(normalized_in_crockford).to_s
  end

  # @return [Boolean]
  def self.normalized?(object)
    normalized = normalize(object)
  rescue Exception
    false
  else
    normalized == object
  end

  # @return [Boolean]
  def self.valid?(object)
    string = String.try_convert(object)
    string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
  end

  # @param [ULID, #to_ulid] object
  # @return [ULID, nil]
  # @raise [TypeError] if `object.to_ulid` did not return ULID instance
  def self.try_convert(object)
    begin
      converted = object.to_ulid
    rescue NoMethodError
      nil
    else
      if ULID === converted
        converted
      else
        object_class_name = safe_get_class_name(object)
        converted_class_name = safe_get_class_name(converted)
        raise TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})"
      end
    end
  end

  # @param [BasicObject] object
  # @return [String]
  private_class_method def self.safe_get_class_name(object)
    fallback = 'UnknownObject'

    # This class getter implementation used https://github.com/rspec/rspec-support/blob/4ad8392d0787a66f9c351d9cf6c7618e18b3d0f2/lib/rspec/support.rb#L83-L89 as a reference, thank you!
    # ref: https://twitter.com/_kachick/status/1400064896759304196
    klass = (
      begin
        object.class
      rescue NoMethodError
        singleton_class = class << object; self; end
        singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) }
      end
    )

    begin
      name = String.try_convert(klass.name)
    rescue Exception
      fallback
    else
      name || fallback
    end
  end

  # @api private
  # @param [Integer] milliseconds
  # @param [Integer] entropy
  # @return [ULID]
  # @raise [OverflowError] if the given value is larger than the ULID limit
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
  def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
    raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
    raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
    raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
    raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?

    n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
    n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
    integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)

    new(milliseconds: milliseconds, entropy: entropy, integer: integer)
  end

  attr_reader :milliseconds, :entropy

  # @api private
  # @param [Integer] milliseconds
  # @param [Integer] entropy
  # @param [Integer] integer
  # @return [void]
  def initialize(milliseconds:, entropy:, integer:)
    # All arguments check should be done with each constructors, not here
    @integer = integer
    @milliseconds = milliseconds
    @entropy = entropy
  end

  # @return [String]
  def to_s
    @string ||= CrockfordBase32.encode(@integer).freeze
  end

  # @return [Integer]
  def to_i
    @integer
  end
  alias_method :hash, :to_i

  # @return [Integer, nil]
  def <=>(other)
    (ULID === other) ? (@integer <=> other.to_i) : nil
  end

  # @return [String]
  def inspect
    @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
  end

  # @return [Boolean]
  def eql?(other)
    equal?(other) || (ULID === other && @integer == other.to_i)
  end
  alias_method :==, :eql?

  # @return [Boolean]
  def ===(other)
    case other
    when ULID
      @integer == other.to_i
    when String
      to_s == other.upcase
    else
      false
    end
  end

  # @return [Time]
  def to_time
    @time ||= begin
      if RUBY_VERSION >= '2.7'
        Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
      else
        Time.at(0, @milliseconds, :millisecond).utc.freeze
      end
    end
  end

  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
  def octets
    digits = @integer.digits(256)
    (OCTETS_LENGTH - digits.size).times do
      digits.push(0)
    end
    digits.reverse!
  end

  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
  def timestamp_octets
    octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
  end

  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
  def randomness_octets
    octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
  end

  # @return [String]
  def timestamp
    @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
  end

  # @return [String]
  def randomness
    @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
  end

  # @note Providing for rough operations. The keys and values is not fixed.
  # @return [Hash{Symbol => Regexp, String}]
  def patterns
    named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
    {
      named_captures: named_captures,
      strict_named_captures: /\A#{named_captures.source}\z/i.freeze
    }
  end

  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
  def succ
    succ_int = @integer.succ
    if succ_int >= MAX_INTEGER
      if succ_int == MAX_INTEGER
        MAX
      else
        nil
      end
    else
      ULID.from_integer(succ_int)
    end
  end
  alias_method :next, :succ

  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
  def pred
    pred_int = @integer.pred
    if pred_int <= 0
      if pred_int == 0
        MIN
      else
        nil
      end
    else
      ULID.from_integer(pred_int)
    end
  end

  # @return [self]
  def freeze
    # Need to cache before freezing, because frozen objects can't assign instance variables
    cache_all_instance_variables
    super
  end

  # @api private
  # @return [Integer]
  def marshal_dump
    @integer
  end

  # @api private
  # @param [Integer] integer
  # @return [void]
  def marshal_load(integer)
    unmarshaled = ULID.from_integer(integer)
    initialize(integer: unmarshaled.to_i, milliseconds: unmarshaled.milliseconds, entropy: unmarshaled.entropy)
  end

  # @return [self]
  def to_ulid
    self
  end

  # @return [self]
  def dup
    self
  end

  # @return [self]
  def clone(freeze: true)
    self
  end

  undef_method :instance_variable_set

  private

  # @return [void]
  def cache_all_instance_variables
    inspect
    timestamp
    randomness
  end
end

#millisecondsInteger (readonly)

Returns:

  • (Integer)


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'lib/ulid.rb', line 13

class ULID
  include Comparable

  class Error < StandardError; end
  class OverflowError < Error; end
  class ParserError < Error; end
  class UnexpectedError < Error; end

  # Excluded I, L, O, U, -.
  # This is the encoding patterns.
  # The decoding issue is written in ULID::CrockfordBase32
  CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'

  TIMESTAMP_ENCODED_LENGTH = 10
  RANDOMNESS_ENCODED_LENGTH = 16
  ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
  TIMESTAMP_OCTETS_LENGTH = 6
  RANDOMNESS_OCTETS_LENGTH = 10
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
  MAX_MILLISECONDS = 281474976710655
  MAX_ENTROPY = 1208925819614629174706175
  MAX_INTEGER = 340282366920938463463374607431768211455

  # @see https://github.com/ulid/spec/pull/57
  # Currently not used as a constant, but kept as a reference for now.
  PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze

  STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze

  # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
  # This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
  SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze

  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
  # @see https://bugs.ruby-lang.org/issues/15958
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'

  private_class_method :new

  # @param [Integer, Time] moment
  # @param [Integer] entropy
  # @return [ULID]
  def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
    from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
  end

  # Short hand of `ULID.generate(moment: time)`
  # @param [Time] time
  # @return [ULID]
  def self.at(time)
    raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time

    from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
  end

  # @param [Time, Integer] moment
  # @return [ULID]
  def self.min(moment=0)
    0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
  end

  # @param [Time, Integer] moment
  # @return [ULID]
  def self.max(moment=MAX_MILLISECONDS)
    MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
  end

  RANDOM_INTEGER_GENERATOR = -> {
    SecureRandom.random_number(MAX_INTEGER)
  }

  # @param [Range<Time>, Range<nil>, Range[ULID], nil] period
  # @overload sample(number, period: nil)
  #   @param [Integer] number
  #   @return [Array<ULID>]
  #   @raise [ArgumentError] if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number
  # @overload sample(period: nil)
  #   @return [ULID]
  # @note Major difference of `Array#sample` interface is below
  #   * Do not ensure the uniqueness
  #   * Do not take random generator for the arguments
  #   * Raising error instead of truncating elements for the given number
  def self.sample(*args, period: nil)
    int_generator = (
      if period
        ulid_range = range(period)
        min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?

        possibilities = (max - min) + (exclude_end ? 0 : 1)
        raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?

        -> {
          SecureRandom.random_number(possibilities) + min
        }
      else
        RANDOM_INTEGER_GENERATOR
      end
    )

    case args.size
    when 0
      from_integer(int_generator.call)
    when 1
      number = args.first
      raise ArgumentError, 'accepts no argument or integer only' unless Integer === number

      if number > MAX_INTEGER || number.negative?
        raise ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative"
      end

      if period && (number > possibilities)
        raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
      end

      Array.new(number) { from_integer(int_generator.call) }
    else
      raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
    end
  end

  # @param [String, #to_str] string
  # @return [Enumerator]
  # @yieldparam [ULID] ulid
  # @yieldreturn [self]
  def self.scan(string)
    string = String.try_convert(string)
    raise ArgumentError, 'ULID.scan takes only strings' unless string
    return to_enum(__callee__, string) unless block_given?

    string.scan(SCANNING_PATTERN) do |matched|
      yield parse(matched)
    end
    self
  end

  # @param [Integer] integer
  # @return [ULID]
  # @raise [OverflowError] if the given integer is larger than the ULID limit
  # @raise [ArgumentError] if the given integer is negative number
  def self.from_integer(integer)
    raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
    raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
    raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?

    n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
    n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
    n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)

    milliseconds = n32encoded_timestamp.to_i(32)
    entropy = n32encoded_randomness.to_i(32)

    new(milliseconds: milliseconds, entropy: entropy, integer: integer)
  end

  # @param [Range<Time>, Range<nil>, Range[ULID]] period
  # @return [Range<ULID>]
  # @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
  def self.range(period)
    raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period

    begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
    return period if self === begin_element && self === end_element

    case begin_element
    when Time
      begin_ulid = min(begin_element)
    when nil
      begin_ulid = MIN
    when self
      begin_ulid = begin_element
    else
      raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
    end

    case end_element
    when Time
      end_ulid = exclude_end ? min(end_element) : max(end_element)
    when nil
      # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
      end_ulid = MAX
      exclude_end = false
    when self
      end_ulid = end_element
    else
      raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
    end

    begin_ulid.freeze
    end_ulid.freeze

    Range.new(begin_ulid, end_ulid, exclude_end)
  end

  # @param [Time] time
  # @return [Time]
  def self.floor(time)
    raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time

    if RUBY_VERSION >= '2.7'
      time.floor(3)
    else
      Time.at(0, milliseconds_from_time(time), :millisecond)
    end
  end

  # @api private
  # @return [Integer]
  def self.current_milliseconds
    milliseconds_from_time(Time.now)
  end

  # @api private
  # @param [Time] time
  # @return [Integer]
  private_class_method def self.milliseconds_from_time(time)
    (time.to_r * 1000).to_i
  end

  # @api private
  # @param [Time, Integer] moment
  # @return [Integer]
  def self.milliseconds_from_moment(moment)
    case moment
    when Integer
      moment
    when Time
      milliseconds_from_time(moment)
    else
      raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
    end
  end

  # @return [Integer]
  private_class_method def self.reasonable_entropy
    SecureRandom.random_number(MAX_ENTROPY)
  end

  # @param [String, #to_str] string
  # @return [ULID]
  # @raise [ParserError] if the given format is not correct for ULID specs
  def self.parse(string)
    string = String.try_convert(string)
    raise ArgumentError, 'ULID.parse takes only strings' unless string

    unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
      raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
    end

    from_integer(CrockfordBase32.decode(string))
  end

  # @param [String, #to_str] string
  # @return [String]
  # @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
  def self.normalize(string)
    string = String.try_convert(string)
    raise ArgumentError, 'ULID.normalize takes only strings' unless string

    normalized_in_crockford = CrockfordBase32.normalize(string)
    # Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
    parse(normalized_in_crockford).to_s
  end

  # @return [Boolean]
  def self.normalized?(object)
    normalized = normalize(object)
  rescue Exception
    false
  else
    normalized == object
  end

  # @return [Boolean]
  def self.valid?(object)
    string = String.try_convert(object)
    string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
  end

  # @param [ULID, #to_ulid] object
  # @return [ULID, nil]
  # @raise [TypeError] if `object.to_ulid` did not return ULID instance
  def self.try_convert(object)
    begin
      converted = object.to_ulid
    rescue NoMethodError
      nil
    else
      if ULID === converted
        converted
      else
        object_class_name = safe_get_class_name(object)
        converted_class_name = safe_get_class_name(converted)
        raise TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})"
      end
    end
  end

  # @param [BasicObject] object
  # @return [String]
  private_class_method def self.safe_get_class_name(object)
    fallback = 'UnknownObject'

    # This class getter implementation used https://github.com/rspec/rspec-support/blob/4ad8392d0787a66f9c351d9cf6c7618e18b3d0f2/lib/rspec/support.rb#L83-L89 as a reference, thank you!
    # ref: https://twitter.com/_kachick/status/1400064896759304196
    klass = (
      begin
        object.class
      rescue NoMethodError
        singleton_class = class << object; self; end
        singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) }
      end
    )

    begin
      name = String.try_convert(klass.name)
    rescue Exception
      fallback
    else
      name || fallback
    end
  end

  # @api private
  # @param [Integer] milliseconds
  # @param [Integer] entropy
  # @return [ULID]
  # @raise [OverflowError] if the given value is larger than the ULID limit
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
  def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
    raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
    raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
    raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
    raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?

    n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
    n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
    integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)

    new(milliseconds: milliseconds, entropy: entropy, integer: integer)
  end

  attr_reader :milliseconds, :entropy

  # @api private
  # @param [Integer] milliseconds
  # @param [Integer] entropy
  # @param [Integer] integer
  # @return [void]
  def initialize(milliseconds:, entropy:, integer:)
    # All arguments check should be done with each constructors, not here
    @integer = integer
    @milliseconds = milliseconds
    @entropy = entropy
  end

  # @return [String]
  def to_s
    @string ||= CrockfordBase32.encode(@integer).freeze
  end

  # @return [Integer]
  def to_i
    @integer
  end
  alias_method :hash, :to_i

  # @return [Integer, nil]
  def <=>(other)
    (ULID === other) ? (@integer <=> other.to_i) : nil
  end

  # @return [String]
  def inspect
    @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
  end

  # @return [Boolean]
  def eql?(other)
    equal?(other) || (ULID === other && @integer == other.to_i)
  end
  alias_method :==, :eql?

  # @return [Boolean]
  def ===(other)
    case other
    when ULID
      @integer == other.to_i
    when String
      to_s == other.upcase
    else
      false
    end
  end

  # @return [Time]
  def to_time
    @time ||= begin
      if RUBY_VERSION >= '2.7'
        Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
      else
        Time.at(0, @milliseconds, :millisecond).utc.freeze
      end
    end
  end

  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
  def octets
    digits = @integer.digits(256)
    (OCTETS_LENGTH - digits.size).times do
      digits.push(0)
    end
    digits.reverse!
  end

  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
  def timestamp_octets
    octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
  end

  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
  def randomness_octets
    octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
  end

  # @return [String]
  def timestamp
    @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
  end

  # @return [String]
  def randomness
    @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
  end

  # @note Providing for rough operations. The keys and values is not fixed.
  # @return [Hash{Symbol => Regexp, String}]
  def patterns
    named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
    {
      named_captures: named_captures,
      strict_named_captures: /\A#{named_captures.source}\z/i.freeze
    }
  end

  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
  def succ
    succ_int = @integer.succ
    if succ_int >= MAX_INTEGER
      if succ_int == MAX_INTEGER
        MAX
      else
        nil
      end
    else
      ULID.from_integer(succ_int)
    end
  end
  alias_method :next, :succ

  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
  def pred
    pred_int = @integer.pred
    if pred_int <= 0
      if pred_int == 0
        MIN
      else
        nil
      end
    else
      ULID.from_integer(pred_int)
    end
  end

  # @return [self]
  def freeze
    # Need to cache before freezing, because frozen objects can't assign instance variables
    cache_all_instance_variables
    super
  end

  # @api private
  # @return [Integer]
  def marshal_dump
    @integer
  end

  # @api private
  # @param [Integer] integer
  # @return [void]
  def marshal_load(integer)
    unmarshaled = ULID.from_integer(integer)
    initialize(integer: unmarshaled.to_i, milliseconds: unmarshaled.milliseconds, entropy: unmarshaled.entropy)
  end

  # @return [self]
  def to_ulid
    self
  end

  # @return [self]
  def dup
    self
  end

  # @return [self]
  def clone(freeze: true)
    self
  end

  undef_method :instance_variable_set

  private

  # @return [void]
  def cache_all_instance_variables
    inspect
    timestamp
    randomness
  end
end

Class Method Details

.at(time) ⇒ ULID

Short hand of `ULID.generate(moment: time)`

Parameters:

  • time (Time)

Returns:

Raises:

  • (ArgumentError)


62
63
64
65
66
# File 'lib/ulid.rb', line 62

def self.at(time)
  raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time

  from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
end

.current_millisecondsInteger

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Integer)


220
221
222
# File 'lib/ulid.rb', line 220

def self.current_milliseconds
  milliseconds_from_time(Time.now)
end

.floor(time) ⇒ Time

Parameters:

  • time (Time)

Returns:

  • (Time)

Raises:

  • (ArgumentError)


208
209
210
211
212
213
214
215
216
# File 'lib/ulid.rb', line 208

def self.floor(time)
  raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time

  if RUBY_VERSION >= '2.7'
    time.floor(3)
  else
    Time.at(0, milliseconds_from_time(time), :millisecond)
  end
end

.from_integer(integer) ⇒ ULID

Parameters:

  • integer (Integer)

Returns:

Raises:

  • (OverflowError)

    if the given integer is larger than the ULID limit

  • (ArgumentError)

    if the given integer is negative number



152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/ulid.rb', line 152

def self.from_integer(integer)
  raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
  raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?

  n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
  n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
  n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)

  milliseconds = n32encoded_timestamp.to_i(32)
  entropy = n32encoded_randomness.to_i(32)

  new(milliseconds: milliseconds, entropy: entropy, integer: integer)
end

.from_milliseconds_and_entropy(milliseconds:, entropy:) ⇒ ULID

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • milliseconds (Integer)
  • entropy (Integer)

Returns:

Raises:

  • (OverflowError)

    if the given value is larger than the ULID limit

  • (ArgumentError)

    if the given milliseconds and/or entropy is negative number



341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/ulid.rb', line 341

def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
  raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
  raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
  raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
  raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?

  n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
  n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
  integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)

  new(milliseconds: milliseconds, entropy: entropy, integer: integer)
end

.from_uuidv4(uuid) ⇒ ULID

Parameters:

  • uuid (String, #to_str)

Returns:

Raises:

  • (ParserError)

    if the given format is not correct for UUIDv4 specs



18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/ulid/uuid.rb', line 18

def self.from_uuidv4(uuid)
  uuid = String.try_convert(uuid)
  raise ArgumentError, 'ULID.from_uuidv4 takes only strings' unless uuid

  prefix_trimmed = uuid.delete_prefix('urn:uuid:')
  unless UUIDV4_PATTERN.match?(prefix_trimmed)
    raise ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`"
  end

  normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
  from_integer(normalized.to_i(16))
end

.generate(moment: current_milliseconds, entropy: reasonable_entropy) ⇒ ULID

Parameters:

  • moment (Integer, Time) (defaults to: current_milliseconds)
  • entropy (Integer) (defaults to: reasonable_entropy)

Returns:



55
56
57
# File 'lib/ulid.rb', line 55

def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
  from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
end

.max(moment = MAX_MILLISECONDS) ⇒ ULID

Parameters:

  • moment (Time, Integer) (defaults to: MAX_MILLISECONDS)

Returns:



76
77
78
# File 'lib/ulid.rb', line 76

def self.max(moment=MAX_MILLISECONDS)
  MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
end

.milliseconds_from_moment(moment) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • moment (Time, Integer)

Returns:

  • (Integer)


234
235
236
237
238
239
240
241
242
243
# File 'lib/ulid.rb', line 234

def self.milliseconds_from_moment(moment)
  case moment
  when Integer
    moment
  when Time
    milliseconds_from_time(moment)
  else
    raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
  end
end

.min(moment = 0) ⇒ ULID

Parameters:

  • moment (Time, Integer) (defaults to: 0)

Returns:



70
71
72
# File 'lib/ulid.rb', line 70

def self.min(moment=0)
  0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
end

.normalize(string) ⇒ String

Parameters:

  • string (String, #to_str)

Returns:

  • (String)

Raises:

  • (ParserError)

    if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`



267
268
269
270
271
272
273
274
# File 'lib/ulid.rb', line 267

def self.normalize(string)
  string = String.try_convert(string)
  raise ArgumentError, 'ULID.normalize takes only strings' unless string

  normalized_in_crockford = CrockfordBase32.normalize(string)
  # Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
  parse(normalized_in_crockford).to_s
end

.normalized?(object) ⇒ Boolean

Returns:

  • (Boolean)


277
278
279
280
281
282
283
# File 'lib/ulid.rb', line 277

def self.normalized?(object)
  normalized = normalize(object)
rescue Exception
  false
else
  normalized == object
end

.parse(string) ⇒ ULID

Parameters:

  • string (String, #to_str)

Returns:

Raises:

  • (ParserError)

    if the given format is not correct for ULID specs



253
254
255
256
257
258
259
260
261
262
# File 'lib/ulid.rb', line 253

def self.parse(string)
  string = String.try_convert(string)
  raise ArgumentError, 'ULID.parse takes only strings' unless string

  unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
    raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
  end

  from_integer(CrockfordBase32.decode(string))
end

.range(period) ⇒ Range<ULID>

Parameters:

  • period (Range<Time>, Range<nil>, Range[ULID])

Returns:

Raises:

  • (ArgumentError)

    if the given period is not a `Range`, `Range` or `Range`



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/ulid.rb', line 170

def self.range(period)
  raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period

  begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
  return period if self === begin_element && self === end_element

  case begin_element
  when Time
    begin_ulid = min(begin_element)
  when nil
    begin_ulid = MIN
  when self
    begin_ulid = begin_element
  else
    raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
  end

  case end_element
  when Time
    end_ulid = exclude_end ? min(end_element) : max(end_element)
  when nil
    # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
    end_ulid = MAX
    exclude_end = false
  when self
    end_ulid = end_element
  else
    raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
  end

  begin_ulid.freeze
  end_ulid.freeze

  Range.new(begin_ulid, end_ulid, exclude_end)
end

.sample(number, period: nil) ⇒ Array<ULID> .sample(period: nil) ⇒ ULID

Note:

Major difference of `Array#sample` interface is below

  • Do not ensure the uniqueness

  • Do not take random generator for the arguments

  • Raising error instead of truncating elements for the given number

Overloads:

  • .sample(number, period: nil) ⇒ Array<ULID>

    Parameters:

    • number (Integer)

    Returns:

    Raises:

    • (ArgumentError)

      if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number

  • .sample(period: nil) ⇒ ULID

    Returns:

Parameters:

  • period (Range<Time>, Range<nil>, Range[ULID], nil) (defaults to: nil)


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/ulid.rb', line 95

def self.sample(*args, period: nil)
  int_generator = (
    if period
      ulid_range = range(period)
      min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?

      possibilities = (max - min) + (exclude_end ? 0 : 1)
      raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?

      -> {
        SecureRandom.random_number(possibilities) + min
      }
    else
      RANDOM_INTEGER_GENERATOR
    end
  )

  case args.size
  when 0
    from_integer(int_generator.call)
  when 1
    number = args.first
    raise ArgumentError, 'accepts no argument or integer only' unless Integer === number

    if number > MAX_INTEGER || number.negative?
      raise ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative"
    end

    if period && (number > possibilities)
      raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
    end

    Array.new(number) { from_integer(int_generator.call) }
  else
    raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
  end
end

.scan(string) {|ulid| ... } ⇒ Enumerator

Parameters:

  • string (String, #to_str)

Yield Parameters:

Yield Returns:

  • (self)

Returns:

  • (Enumerator)

Raises:

  • (ArgumentError)


137
138
139
140
141
142
143
144
145
146
# File 'lib/ulid.rb', line 137

def self.scan(string)
  string = String.try_convert(string)
  raise ArgumentError, 'ULID.scan takes only strings' unless string
  return to_enum(__callee__, string) unless block_given?

  string.scan(SCANNING_PATTERN) do |matched|
    yield parse(matched)
  end
  self
end

.try_convert(object) ⇒ ULID?

Parameters:

Returns:

Raises:

  • (TypeError)

    if `object.to_ulid` did not return ULID instance



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/ulid.rb', line 294

def self.try_convert(object)
  begin
    converted = object.to_ulid
  rescue NoMethodError
    nil
  else
    if ULID === converted
      converted
    else
      object_class_name = safe_get_class_name(object)
      converted_class_name = safe_get_class_name(converted)
      raise TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})"
    end
  end
end

.valid?(object) ⇒ Boolean

Returns:

  • (Boolean)


286
287
288
289
# File 'lib/ulid.rb', line 286

def self.valid?(object)
  string = String.try_convert(object)
  string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
end

Instance Method Details

#<=>(other) ⇒ Integer?

Returns:

  • (Integer, nil)


380
381
382
# File 'lib/ulid.rb', line 380

def <=>(other)
  (ULID === other) ? (@integer <=> other.to_i) : nil
end

#===(other) ⇒ Boolean

Returns:

  • (Boolean)


396
397
398
399
400
401
402
403
404
405
# File 'lib/ulid.rb', line 396

def ===(other)
  case other
  when ULID
    @integer == other.to_i
  when String
    to_s == other.upcase
  else
    false
  end
end

#clone(freeze: true) ⇒ self

Returns:

  • (self)


518
519
520
# File 'lib/ulid.rb', line 518

def clone(freeze: true)
  self
end

#dupself

Returns:

  • (self)


513
514
515
# File 'lib/ulid.rb', line 513

def dup
  self
end

#eql?(other) ⇒ Boolean Also known as: ==

Returns:

  • (Boolean)


390
391
392
# File 'lib/ulid.rb', line 390

def eql?(other)
  equal?(other) || (ULID === other && @integer == other.to_i)
end

#freezeself

Returns:

  • (self)


487
488
489
490
491
# File 'lib/ulid.rb', line 487

def freeze
  # Need to cache before freezing, because frozen objects can't assign instance variables
  cache_all_instance_variables
  super
end

#inspectString

Returns:

  • (String)


385
386
387
# File 'lib/ulid.rb', line 385

def inspect
  @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
end

#marshal_dumpInteger

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Integer)


495
496
497
# File 'lib/ulid.rb', line 495

def marshal_dump
  @integer
end

#marshal_load(integer) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Parameters:

  • integer (Integer)


502
503
504
505
# File 'lib/ulid.rb', line 502

def marshal_load(integer)
  unmarshaled = ULID.from_integer(integer)
  initialize(integer: unmarshaled.to_i, milliseconds: unmarshaled.milliseconds, entropy: unmarshaled.entropy)
end

#octetsArray(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)

Returns:

  • (Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer))


419
420
421
422
423
424
425
# File 'lib/ulid.rb', line 419

def octets
  digits = @integer.digits(256)
  (OCTETS_LENGTH - digits.size).times do
    digits.push(0)
  end
  digits.reverse!
end

#patternsHash{Symbol => Regexp, String}

Note:

Providing for rough operations. The keys and values is not fixed.

Returns:

  • (Hash{Symbol => Regexp, String})


449
450
451
452
453
454
455
# File 'lib/ulid.rb', line 449

def patterns
  named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
  {
    named_captures: named_captures,
    strict_named_captures: /\A#{named_captures.source}\z/i.freeze
  }
end

#predULID?

Returns when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID.

Returns:

  • (ULID, nil)

    when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID



473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/ulid.rb', line 473

def pred
  pred_int = @integer.pred
  if pred_int <= 0
    if pred_int == 0
      MIN
    else
      nil
    end
  else
    ULID.from_integer(pred_int)
  end
end

#randomnessString

Returns:

  • (String)


443
444
445
# File 'lib/ulid.rb', line 443

def randomness
  @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
end

#randomness_octetsArray(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)

Returns:

  • (Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer))


433
434
435
# File 'lib/ulid.rb', line 433

def randomness_octets
  octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
end

#succULID? Also known as: next

Returns when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID.

Returns:

  • (ULID, nil)

    when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID



458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/ulid.rb', line 458

def succ
  succ_int = @integer.succ
  if succ_int >= MAX_INTEGER
    if succ_int == MAX_INTEGER
      MAX
    else
      nil
    end
  else
    ULID.from_integer(succ_int)
  end
end

#timestampString

Returns:

  • (String)


438
439
440
# File 'lib/ulid.rb', line 438

def timestamp
  @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
end

#timestamp_octetsArray(Integer, Integer, Integer, Integer, Integer, Integer)

Returns:

  • (Array(Integer, Integer, Integer, Integer, Integer, Integer))


428
429
430
# File 'lib/ulid.rb', line 428

def timestamp_octets
  octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
end

#to_iInteger Also known as: hash

Returns:

  • (Integer)


374
375
376
# File 'lib/ulid.rb', line 374

def to_i
  @integer
end

#to_sString

Returns:

  • (String)


369
370
371
# File 'lib/ulid.rb', line 369

def to_s
  @string ||= CrockfordBase32.encode(@integer).freeze
end

#to_timeTime

Returns:

  • (Time)


408
409
410
411
412
413
414
415
416
# File 'lib/ulid.rb', line 408

def to_time
  @time ||= begin
    if RUBY_VERSION >= '2.7'
      Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
    else
      Time.at(0, @milliseconds, :millisecond).utc.freeze
    end
  end
end

#to_ulidself

Returns:

  • (self)


508
509
510
# File 'lib/ulid.rb', line 508

def to_ulid
  self
end

#to_uuidv4String

Returns:

  • (String)


32
33
34
35
36
37
38
# File 'lib/ulid/uuid.rb', line 32

def to_uuidv4
  # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
  array = octets.pack('C*').unpack('NnnnnN')
  array[2] = (array[2] & 0x0fff) | 0x4000
  array[3] = (array[3] & 0x3fff) | 0x8000
  ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
end