@@ -10,14 +10,17 @@ module EncodedId
1010 class ReversibleId
1111 ALPHABET = "0123456789abcdefghjkmnpqrstuvwxyz"
1212
13- def initialize ( salt :, length : 8 , split_at : 4 , alphabet : ALPHABET )
13+ def initialize ( salt :, length : 8 , split_at : 4 , alphabet : ALPHABET , hex_digit_encoding_group_size : 4 )
1414 unique_alphabet = alphabet . chars . uniq
1515 raise InvalidAlphabetError , "Alphabet must be at least 16 characters" if unique_alphabet . size < 16
1616
1717 @human_friendly_alphabet = unique_alphabet . join
1818 @salt = salt
1919 @length = length
2020 @split_at = split_at
21+ # Number of hex digits to encode in each group, larger values will result in shorter hashes for longer inputs.
22+ # Vice versa for smaller values, ie a smaller value will result in smaller hashes for small inputs.
23+ @hex_digit_encoding_group_size = hex_digit_encoding_group_size
2124 end
2225
2326 # Encode the input values into a hash
@@ -28,16 +31,27 @@ def encode(values)
2831 encoded_id
2932 end
3033
34+ # Encode hex strings into a hash
35+ def encode_hex ( hexs )
36+ encode ( integer_representation ( hexs ) )
37+ end
38+
3139 # Decode the hash to original array
3240 def decode ( str )
3341 encoded_id_generator . decode ( convert_to_hash ( str ) )
3442 rescue ::Hashids ::InputError => e
3543 raise EncodedIdFormatError , e . message
3644 end
3745
46+ # Decode hex strings from a hash
47+ def decode_hex ( str )
48+ integers = encoded_id_generator . decode ( convert_to_hash ( str ) )
49+ integers_to_hex_strings ( integers )
50+ end
51+
3852 private
3953
40- attr_reader :salt , :length , :human_friendly_alphabet , :split_at
54+ attr_reader :salt , :length , :human_friendly_alphabet , :split_at , :hex_digit_encoding_group_size
4155
4256 def prepare_input ( value )
4357 inputs = value . is_a? ( Array ) ? value . map ( &:to_i ) : [ value . to_i ]
@@ -67,5 +81,53 @@ def map_crockford_set(str)
6781 # only use lowercase
6882 str . tr ( "o" , "0" ) . tr ( "l" , "1" ) . tr ( "i" , "j" )
6983 end
84+
85+ # TODO: optimize this
86+ def integer_representation ( hexs )
87+ inputs = hexs . is_a? ( Array ) ? hexs . map ( &:to_s ) : [ hexs . to_s ]
88+ inputs . map! do |hex_string |
89+ cleaned = hex_string . gsub ( /[^0-9a-f]/i , "" )
90+ # Convert to groups of integers. Process least significant hex digits first
91+ groups = [ ]
92+ cleaned . chars . reverse . each_with_index do |char , i |
93+ group_id = i / hex_digit_encoding_group_size . to_i
94+ groups [ group_id ] ||= [ ]
95+ groups [ group_id ] . unshift ( char )
96+ end
97+ groups . map { |c | c . join . to_i ( 16 ) }
98+ end
99+ digits_to_encode = [ ]
100+ inputs . each_with_object ( digits_to_encode ) do |hex_digits , digits |
101+ digits . concat ( hex_digits )
102+ digits << hex_string_separator
103+ end
104+ digits_to_encode . pop unless digits_to_encode . empty? # Remove the last marker
105+ digits_to_encode
106+ end
107+
108+ # Marker to separate hex strings, must be greater than largest value encoded
109+ def hex_string_separator
110+ @hex_string_separator ||= 2 . pow ( hex_digit_encoding_group_size * 4 ) + 1
111+ end
112+
113+ # TODO: optimize this
114+ def integers_to_hex_strings ( integers )
115+ hex_strings = [ ]
116+ hex_string = [ ]
117+ add_leading = false
118+ # Digits are encoded in least significant digit first order, but string is most significant first, so reverse
119+ integers . reverse_each do |integer |
120+ if integer == hex_string_separator # Marker to separate hex strings, so start a new one
121+ hex_strings << hex_string . join
122+ hex_string = [ ]
123+ add_leading = false
124+ else
125+ hex_string << ( add_leading ? "%.#{ hex_digit_encoding_group_size } x" % integer : integer . to_s ( 16 ) )
126+ add_leading = true
127+ end
128+ end
129+ hex_strings << hex_string . join unless hex_string . empty? # Add the last hex string
130+ hex_strings . reverse # Reverse final values to get the original order (the encoding process also reverses the encoded value order)
131+ end
70132 end
71133end
0 commit comments