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
-
.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
. -
.collated_hash_from(array, dupes = false) ⇒ Object
A very single-purpose method which converts an Array of specifc form into a Hash.
-
.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).
-
.deep_merge_into(target_hash, inbound_hash) ⇒ Object
Deep merge two hashes.
-
.hash_diff(hash1, hash2) ⇒ Object
Deep diff two hashes.
-
.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.
-
.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
? Returnstrue
if so, elsefalse
. -
.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).
-
.rationalise_datetime(input) ⇒ Object
Turn a given value of various types into a DateTime instance or
nil
. -
.spare_port ⇒ Object
Return a spare TCP port on localhost.
-
.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.
-
.stringify(obj) ⇒ Object
The keys-to-strings equivalent of ::symbolize.
-
.symbolize(obj) ⇒ Object
Given a hash, returns the same hash with keys converted to symbols.
-
.to_integer?(value) ⇒ Boolean
Is a parameter convertable to an integer cleanly? Returns the integer value if so, else
nil
. -
.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?.
-
.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?.
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.
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
, returnsnil
.
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_port ⇒ Object
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).
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
.
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
.
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 |