diff --git a/README.md b/README.md index c94ab51..02e58a8 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,17 @@ ulid2 = ULID.generate(time, suffix: an_event_identifier) ulid1 == ulid2 # true ``` +**I want to decode the timestamp portion of an existing ULID value** + +You can also decode the timestamp component of a ULID into a `Time` instance (to millisecond precision). + +```ruby +time_t1 = Time.new(2022, 1, 4, 6, 3) +ulid = ULID.generate(time_t1) +time_t2 = ULID.decode_time(ulid) +time_t2 == time_t1 # true +``` + ## Specification Below is the current specification of ULID as implemented in this repository. *Note: the binary format has not been implemented.* diff --git a/lib/ulid.rb b/lib/ulid.rb index 6d65fea..d3007ea 100644 --- a/lib/ulid.rb +++ b/lib/ulid.rb @@ -1,6 +1,8 @@ require 'ulid/version' require 'ulid/generator' +require 'ulid/decoder' module ULID extend Generator + extend Decoder end diff --git a/lib/ulid/decoder.rb b/lib/ulid/decoder.rb new file mode 100644 index 0000000..c5dff98 --- /dev/null +++ b/lib/ulid/decoder.rb @@ -0,0 +1,26 @@ +# frozen-string-literal: true + +require 'ulid/generator' + +module ULID + module Decoder + def decode_time(string) + if string.size != Generator::ENCODED_LENGTH + raise ArgumentError, 'string is not of ULID length' + end + + epoch_time_ms = string.slice(0, 10).split('').reverse.each_with_index.reduce(0) do |carry, (char, index)| + encoding_index = Generator::ENCODING.index(char.bytes.first) + + if encoding_index.nil? + raise ArgumentError, "invalid character found: #{char}" + end + + carry += encoding_index * (Generator::ENCODING.size**index).to_i + carry + end + + Time.at(0, epoch_time_ms, :millisecond) + end + end +end diff --git a/spec/lib/ulid_spec.rb b/spec/lib/ulid_spec.rb index 67004cf..9e1e5fb 100644 --- a/spec/lib/ulid_spec.rb +++ b/spec/lib/ulid_spec.rb @@ -103,4 +103,31 @@ end end end + + describe 'decoding a timestamp' do + it 'decodes the timestamp as Time' do + ulid = ULID.generate + assert ULID.decode_time(ulid).is_a?(Time) + end + + it 'decodes the timestamp into milliseconds' do + input_time = Time.now.utc + ulid = ULID.generate(input_time) + output_time = ULID.decode_time(ulid).utc + assert_equal (input_time.to_r * 1000).to_i, (output_time.to_r * 1000).to_i + end + + it 'rejects strings with invalid characters' do + assert_raises(ArgumentError) do + bad_ulid = ULID.generate.tr('0', '-') + ULID.decode_time(bad_ulid) + end + end + + it 'rejects strings of incorrect length' do + assert_raises(ArgumentError) do + ULID.decode_time(ULID.generate + 'f') + end + end + end end