Module: Hoodoo::Utilities

Defined in:
lib/hoodoo/utilities/utilities.rb

Overview

Useful tools, especially for those working without Rails components.

Constant Summary collapse

DATETIME_ISO8601_SUBSET_REGEXP =

Validation regular expression for DateTime subset selection.

/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(\.\d+)?(Z|[+-](\d{2})\:(\d{2}))/
DATE_ISO8601_SUBSET_REGEXP =

Validation regular expression for Date subset selection.

/(\d{4})-(\d{2})-(\d{2})/

Class Method Summary collapse

Class Method Details

.clear_clock_drift_configuration_cache!Object

This method is intended really just for testing purposes; it clears the internal cache of clock drift tolerance read from environment variable HOODOO_CLOCK_DRIFT_TOLERANCE.



389
390
391
# File 'lib/hoodoo/utilities/utilities.rb', line 389

def self.clear_clock_drift_configuration_cache!
  @@clock_drift_tolerance = nil
end

.collated_hash_from(array, dupes = false) ⇒ Object

A very single-purpose method which converts an Array of specifc form into a Hash.

The Hash class can already build itself from an Array of tuples, thus:

array = [ [ :one, 1 ], [ :two, 2 ] ]
hash  = Hash[ array ]
# => { :one => 1, :two => 2 }

This is fine, but what if the array contains the same key twice?

array = [ [ :one, 1 ], [ :two, 2 ], [ :one, 42 ] ]
hash  = Hash[ array ]
# => { :one => 42, :two => 2 }

The later duplicates simply override any former entries. This Array collation method is designed to instead take the tuples and set up a Hash where each key leads to an Array of unique values found in the original, thus:

array = [ [ :one, 1 ], [ :two, 2 ], [ :one, 42 ] ]
Hoodoo::Utilities.collated_hash_from( array )
# => { :one => [ 1, 42 ], :two => [ 2 ] }

Note that:

  • The Hash values are always Arrays, even if they only have one value.

  • The Array values are unique; duplicates are removed via uniq!.

array

Array of two-element Arrays. The first element becomes a key in the returned Hash. The last element is added to an Array of (unique) values associated with that key. An empty Array results in an empty Hash; nil is not allowed.

dupes

Optional. If omitted, duplicates are removed as described; if present and true, duplicates are allowed.

Returns a new Hash as described. The input Array is not modified.



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

def self.collated_hash_from( array, dupes = false )
  hash_of_arrays = {}

  array.reduce( hash_of_arrays ) do | memo, sub_array |
    memo[ sub_array.first ] = ( memo[ sub_array.first ] || [] ) << sub_array.last
    memo
  end

  hash_of_arrays.values.collect( &:uniq! ) unless dupes == true
  return hash_of_arrays
end

.deep_dup(obj) ⇒ Object

Thorough but slow deep duplication of any object (if it isn’t duplicable, e.g. FixNum, you just get the same thing back). Usually used with Hashes or Arrays.

obj

Object to duplicate.

Returns the duplicated object if duplicable, else returns the input parameter.



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/hoodoo/utilities/utilities.rb', line 69

def self.deep_dup( obj )
  duplicate = obj.dup rescue obj

  result = if duplicate.is_a?( Hash )
    duplicate.each { | k, v | duplicate[ k ] = deep_dup( v ) }
  elsif duplicate.is_a?( Array )
    duplicate.map { | entry | deep_dup( entry ) }
  else
    duplicate
  end

  return result
end

.deep_merge_into(target_hash, inbound_hash) ⇒ Object

Deep merge two hashes.

Hash#merge/merge! only do a shallow merge. For example, without a block, when starting with this hash:

{ :one => { :two => { :three => 3 } } }

…and merging in this hash:

{ :one => { :two => { :and_four => 4 } } }

…the possibly unexpected result is this:

{ :one => { :two => { :and_four => 4 } } }

Because the value for key “:one” in the original hash is simply overwritten with the value from the merged-in hash.

Deep merging takes a target hash, into which an “inbound” source hash is merged and returns a new hash that is the deep merged result. Taking the above example:

target_hash  = { :one => { :two => { :three => 3 } } }
inbound_hash = { :one => { :two => { :and_four => 4 } } }
Hoodoo::Utilities.deep_merge_into( target_hash, inbound_hash )

…yields:

{ :one => { :two => { :three => 3, :and_four => 4 } } }

For any same-named key with a non-hash value, the value in the inbound hash will overwrite the value in the target hash.

Parameters:

target_hash

The hash into which something will be merged.

inbound_hash

The hash that will be merged into the target.

Returns the merged result.



123
124
125
126
127
128
129
130
131
132
# File 'lib/hoodoo/utilities/utilities.rb', line 123

def self.deep_merge_into( target_hash, inbound_hash )

  # http://stackoverflow.com/questions/9381553/ruby-merge-nested-hash
  #
  merger = proc { | key, v1, v2 |
    v1.is_a?( Hash ) && v2.is_a?( Hash ) ? v1.merge( v2, &merger ) : v2.nil? ? v1 : v2
  }

  return target_hash.merge( inbound_hash, &merger )
end

.hash_diff(hash1, hash2) ⇒ Object

Deep diff two hashes.

hash1

“Left hand” hash for comparison.

hash2

“Right hand” hash for comparison.

The returned result is a Hash itself, potentially nested, with any present key paths leading to an array describing the difference found at that key path. If the two input hashes had values at the path, the differing values are placed in the array (“left hand” value at index 0, “right hand” at index 1). If one input hash has a key leading to a value which the other omits, the array contains nil for the omitted entry.

Example:

hash1 = { :foo => { :bar => 2 }, :baz => true, :boo => false }
hash2 = { :foo => { :bar => 3 },               :boo => false }

Hoodoo::Utilities.hash_diff( hash1, hash2 )
# => { :foo => { :bar => [ 2, 3 ] }, :baz => [ true, nil ] }

Hoodoo::Utilities.hash_diff( hash2, hash1 )
# => { :foo => { :bar => [ 3, 2 ] }, :baz => [ nil, true ] }

Bear in mind that the difference array contains values of everything different from the first part of the key path where things diverge. So in this case:

hash1 = { :foo => { :bar => { :baz => [ 1, 2, 3 ] } } }
hash2 = {}

…the difference starts all the way up at “:foo”. The result is thus not a Hash where just the “:baz” array is picked out as a difference; the entire Hash sub-tree is picked out:

diff = Hoodoo::Utilities.hash_diff( hash1, hash2 )
# => { :foo => [ { :bar => { :baz => [ 1, 2, 3 ] } }, nil ] }


172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/hoodoo/utilities/utilities.rb', line 172

def self.hash_diff( hash1, hash2 )

  # http://stackoverflow.com/questions/1766741/comparing-ruby-hashes
  #
  return ( hash1.keys | hash2.keys ).inject( {} ) do | memo, key |
    unless hash1[ key ] == hash2[ key ]
      if hash1[ key ].kind_of?( Hash ) && hash2[ key ].kind_of?( Hash )
        memo[ key ] = hash_diff( hash1[ key ], hash2[ key ] )
      else
        memo[ key ] = [ hash1[ key ], hash2[ key ] ]
      end
    end

    memo
  end
end

.hash_key_paths(hash) ⇒ Object

Convert a (potentially nested) Hash into an array of entries which represent its keys, with the notation “foo.bar.baz” for nested hashes.

hash

Input Hash.

Example:

hash = { :foo => 1, :bar => { :baz => 2, :boo => { :hello => :world } } }

Hoodoo::Utilities.hash_key_paths( hash )
# => [ 'foo', 'bar.baz', 'bar.boo.hello' ]


201
202
203
204
205
206
207
208
209
210
211
# File 'lib/hoodoo/utilities/utilities.rb', line 201

def self.hash_key_paths( hash )
  return hash.map do | key, value |
    if value.is_a?( Hash )
      hash_key_paths( value ).map do | entry |
        "#{ key }.#{ entry }"
      end
    else
      key.to_s
    end
  end.flatten
end

.is_in_future?(input, backdated_to = DateTime.now) ⇒ Boolean

Is the given Time, DateTime or String instance specifying a date-time that is in the future relevant to this executing environment’s concept of “now”, allowing for CLOCK_DRIFT_TOLERANCE? Returns true if so, else false.

Future date (clock drift) tolerance is specified in seconds using the HOODOO_CLOCK_DRIFT_TOLERANCE environment variable. The default is 30 seconds, as described in the relevant Hoodoo Guide:

http://loyaltynz.github.io/hoodoo/guides_1000_env_vars.html#hoodoo_clock_drift_tolerance
input

A value that’s passed to ::rationalise_datetime with the result checked against ‘now` allowing for clock drift.

backdated_to

Optional. The “now” of the context, defaulting to DateTime.now.

If the given input or ‘backdated_to` is not parseable as a date-time like object, then the method it will throw a RuntimeError exception via ::rationalise_datetime.

Returns:

  • (Boolean)


364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/hoodoo/utilities/utilities.rb', line 364

def self.is_in_future?( input, backdated_to=DateTime.now )

  # See also ::clear_clock_drift_configuration_cache!.
  #
  @@clock_drift_tolerance ||= (
    ENV[ 'HOODOO_CLOCK_DRIFT_TOLERANCE' ].nil? ?
                                            30 :
    ENV[ 'HOODOO_CLOCK_DRIFT_TOLERANCE' ].to_i
  )

  value = self.rationalise_datetime( input )

  backdated_now = self.rationalise_datetime( backdated_to )

  # Unlike Time, integer addition to DateTime adds days. There are 86400
  # seconds per day, so below we add @@clock_drift_tolerance seconds.
  #
  value > backdated_now + Rational(@@clock_drift_tolerance, 86400)

end

.nanosecond_iso8601(time_or_date_time) ⇒ Object

Returns an ISO 8601 String equivalent of the given Time or DateTime instance, with nanosecond precision (subject to Ruby port / OS support). This is nothing more than a standardised central interface on calling Ruby’s Time/DateTime#iso8601( 9 ), to avoid the risk of lots of variable length precision times floating around by authors picking their own arbitrary precision parameters.

date_time

Ruby Time or DateTime instance to convert to an ISO 8601 String with nanosecond precision.



413
414
415
# File 'lib/hoodoo/utilities/utilities.rb', line 413

def self.nanosecond_iso8601( time_or_date_time )
  time_or_date_time.iso8601( 9 )
end

.rationalise_datetime(input) ⇒ Object

Turn a given value of various types into a DateTime instance or nil. If the input value is not nil, a DateTime instance, a Time instance, a Date instance or something else that DateTime.parse can handle, the method will throw a RuntimeError exception.

input

A Time, Date or DateTime instance, or a String that can be converted to a DateTime instance; in these cases, an equivalent DateTime is returned. If nil, returns nil.



426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/hoodoo/utilities/utilities.rb', line 426

def self.rationalise_datetime( input )
  begin
    if input.nil? || input.is_a?( DateTime )
      input
    elsif input.is_a?( Time ) || input.is_a?( Date )
      input.to_datetime
    else
      DateTime.parse( input )
    end

  rescue
    raise "Hoodoo::Utilities\#rationalise_datetime: Invalid parameter '#{ input }'"

  end
end

.spare_portObject

Return a spare TCP port on localhost. This is free at the instant of calling, though of course if you have anything in other local machine processes/threads running which might start using ports at any moment, there’s a chance of the free port getting claimed in between you asking for it and it being returned. This utility method is usually therefore used for test environments only.



282
283
284
285
286
287
288
289
290
291
292
# File 'lib/hoodoo/utilities/utilities.rb', line 282

def self.spare_port

  # http://stackoverflow.com/questions/5985822/how-do-you-find-a-random-open-port-in-ruby
  #
  socket = Socket.new( :INET, :STREAM, 0 )
  socket.bind( Addrinfo.tcp( '127.0.0.1', 0 ) )
  port = socket.local_address.ip_port
  socket.close

  return port
end

.standard_datetime(date_or_time_or_date_time) ⇒ Object

Returns an ISO 8601 String equivalent of the given Time, Date or DateTime instance as a full date-time with UTC timezone, with “standard high precision” (for rendeirng consistency), subject to Ruby port / OS support abilities. At the time of writing, 6 decimal places are included.



399
400
401
# File 'lib/hoodoo/utilities/utilities.rb', line 399

def self.standard_datetime( date_or_time_or_date_time )
  date_or_time_or_date_time.to_time.utc.iso8601( 6 )
end

.stringify(obj) ⇒ Object

The keys-to-strings equivalent of ::symbolize.

obj

Hash or Array of Hashes. Will recursively convert keys in Hashes to strings. Hashes with values that are Arrays of Hashes will be dealt with properly. Does not modify other types (e.g. an Array of Symbols would stay an Array of Symbols).

Returns a copy of your input object with keys converted to strings.



54
55
56
57
58
# File 'lib/hoodoo/utilities/utilities.rb', line 54

def self.stringify(obj)
  return obj.inject({}){|memo,(k,v)| memo[k.to_s] =  self.stringify(v); memo} if obj.is_a?(::Hash)
  return obj.inject([]){|memo,v    | memo         << self.stringify(v); memo} if obj.is_a?(::Array)
  return obj
end

.symbolize(obj) ⇒ Object

Given a hash, returns the same hash with keys converted to symbols. Works with nested hashes.

obj

Hash or Array of Hashes. Will recursively convert keys in Hashes to symbols. Hashes with values that are Arrays of Hashes will be dealt with properly. Does not modify other types (e.g. an Array of Strings would stay an Array of Strings).

Returns a copy of your input object with keys converted to symbols.



36
37
38
39
40
41
42
43
# File 'lib/hoodoo/utilities/utilities.rb', line 36

def self.symbolize(obj)

  # http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
  #
  return obj.inject({}){|memo,(k,v)| memo[k.to_s.to_sym] =  self.symbolize(v); memo} if obj.is_a?(::Hash)
  return obj.inject([]){|memo,v    | memo                << self.symbolize(v); memo} if obj.is_a?(::Array)
  return obj
end

.to_integer?(value) ⇒ Boolean

Is a parameter convertable to an integer cleanly? Returns the integer value if so, else nil.

value

Value to check, e.g. 2, “44”, :‘55’ (yields 2, 44, 55) or “hello”, Time.now (yields nil, nil).

Returns:

  • (Boolean)


270
271
272
273
# File 'lib/hoodoo/utilities/utilities.rb', line 270

def self.to_integer?( value )
  value = value.to_s
  value.to_i if value.to_i.to_s == value
end

.valid_iso8601_subset_date?(str) ⇒ Boolean

Is the given String a valid ISO 8601 subset date (no time) as accepted by (for example) Hoodoo API calls?

str

Value to check

Returns a Date instance holding the parsed result if a valid ISO 8601 subset date, else false.

Returns:

  • (Boolean)


327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/hoodoo/utilities/utilities.rb', line 327

def self.valid_iso8601_subset_date?( str )
  return false unless str.is_a?( String ) || str.is_a?( Symbol )

  # Same reliance as 'valid_iso8601_subset_datetime'?.

  value = begin
    ( DATE_ISO8601_SUBSET_REGEXP =~ str.to_s ) == 0 &&
    str.size == 10                                  &&
    ::Date.parse( str )

  rescue ArgumentError
  end

  return value.is_a?( ::Date ) && value
end

.valid_iso8601_subset_datetime?(str) ⇒ Boolean

Is the given String a valid ISO 8601 subset date and time as accepted by (for example) Hoodoo API calls?

str

Value to check

Returns a DateTime instance holding the parsed result if a valid ISO 8601 subset date and time, else false.

Returns:

  • (Boolean)


302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/hoodoo/utilities/utilities.rb', line 302

def self.valid_iso8601_subset_datetime?( str )
  return false unless str.is_a?( String ) || str.is_a?( Symbol )

  # Relies on Ruby evaluation behaviour and operator precedence - "'foo'
  # && true" => true, but "true && 'foo'" => 'foo'. Don't use "and" here!

  value = begin
    ( DATETIME_ISO8601_SUBSET_REGEXP =~ str.to_s ) == 0 &&
    str.size > 10                                       &&
    ::DateTime.parse( str )

  rescue ArgumentError
  end

  return value.is_a?( ::DateTime ) && value
end