module BBLib

Similar to Ruby's OpenStruct but uses hash as its parent class providing all of the typical Hash methods. Useful for storing settings on objects.

String Comparison Algorithms

Constants

EXTRACT_NUMBER_REGEXP
EXTRACT_NUMBER_REGEXP_NO_INNER
FILE_SIZES
NUMBER_WORDS
REGEXP_MODE_HASH
REGEXP_OPTIONS
ROMAN_NUMERALS
SPECIAL_PLURALS
TIME_EXPS
VERSION

Public Class Methods

are_all?(klass, *objects) click to toggle source

Easy way to see if all objects in an array are of a given class.

# File lib/bblib/core/util/object.rb, line 3
def self.are_all?(klass, *objects)
  objects.all? { |object| object.is_a?(klass) }
end
are_any?(klass, *objects) click to toggle source

Easy way to see if any of the passed objects are of the given class.

# File lib/bblib/core/util/object.rb, line 8
def self.are_any?(klass, *objects)
  objects.any? { |object| object.is_a?(klass) }
end
average(ary) click to toggle source

Takes an array and averages all of the floats and integers within it. Non numeric values are ignored.

# File lib/bblib/core/util/array.rb, line 31
def self.average(ary)
  numbers = ary.select { |v| BBLib.is_any?(v, Integer, Float) }
  numbers.inject(0) do |sum, x|
    sum += x
  end / numbers.size.to_f
end
binary?(file, bytes: 1024, ctrl_threshold: 0.5, binary_threshold: 0.05) click to toggle source

Basic detection for whether or not a file is binary or not

# File lib/bblib/core/util/file.rb, line 109
def self.binary?(file, bytes: 1024, ctrl_threshold: 0.5, binary_threshold: 0.05)
  ascii  = 0
  ctrl   = 0
  binary = 0

  read_bytes = File.open(file, 'rb') { |io| io.read(bytes) }

  return false if read_bytes.nil? || read_bytes.empty?

  read_bytes.each_byte do |byte|
    case byte
    when 0..31
      ctrl += 1
    when 32..127
      ascii += 1
    else
      binary += 1
    end
  end

  ctrl.to_f / ascii > ctrl_threshold || binary.to_f / ascii > binary_threshold
end
camel_case(str, style = :lower) click to toggle source
# File lib/bblib/core/util/cases.rb, line 37
def self.camel_case(str, style = :lower)
  regx = /[[:space:]]+|[^[[:alnum:]]]+/
  words = str.split(regx).map(&:capitalize)
  words[0].downcase! if style == :lower
  words.join
end
chars_up_to(str, cap, too_long = '...', style: :front) click to toggle source

Displays a portion of an object (as a string) with an ellipse displayed if the string is over a certain size. Supported styles:

> front - “for exam…”

> back - “… example”

> middle - “… exam…”

> outter - “for e…ple”

The length of the too_long string is NOT factored into the cap

# File lib/bblib/core/util/string.rb, line 66
def self.chars_up_to(str, cap, too_long = '...', style: :front)
  return str if str.to_s.size <= cap
  str = str.to_s
  case style
  when :back
    "#{too_long}#{str[(str.size - cap)..-1]}"
  when :outter
    "#{str[0...(cap / 2).to_i + (cap.odd? ? 1 : 0)]}#{too_long}#{str[-(cap / 2).to_i..-1]}"
  when :middle
    "#{too_long}#{str[(str.size / 2 - cap / 2 - (cap.odd? ? 1 : 0)).to_i...(str.size / 2 + cap / 2).to_i]}#{too_long}"
  else
    "#{str[0...cap]}#{too_long}"
  end
end
class_case(str) click to toggle source
# File lib/bblib/core/util/cases.rb, line 57
def self.class_case(str)
  str.gsub(/(?<=[^^])([A-Z])/, ' \1').gsub(/\s+/, ' ').title_case.gsub(/\s+|\_/, '')
end
class_create(name, *args, **opts, &block) click to toggle source
# File lib/bblib/core/util/object.rb, line 114
def self.class_create(name, *args, **opts, &block)
  const_create(name, Class.new(*args), **opts, &block)
end
color_logs() click to toggle source
# File lib/bblib/core/util/logging.rb, line 36
def self.color_logs
  @color_logs
end
color_logs=(toggle) click to toggle source
# File lib/bblib/core/util/logging.rb, line 40
def self.color_logs=(toggle)
  @color_logs = (toggle ? true : false)
end
composition_similarity(a, b) click to toggle source

Calculates a percentage based match of two strings based on their character composition.

# File lib/bblib/core/util/matching.rb, line 28
def self.composition_similarity(a, b)
  if a.length <= b.length
    t = a
    a = b
    b = t
  end
  matches = 0
  temp = b.dup
  a.chars.each do |c|
    if temp.chars.include? c
      matches+=1
      temp = temp.sub(c, '')
    end
  end
  (matches / [a.length, b.length].max.to_f)* 100.0
end
const_create(name, value, strict: true, base: Object, type_of_missing: nil, &block) click to toggle source

Create a new class or module recursively within a provided namespace. If a constant matching the requested one already exist it is returned. Any block passed to this method will be evaled in the created/found constant.

# File lib/bblib/core/util/object.rb, line 90
def self.const_create(name, value, strict: true, base: Object, type_of_missing: nil, &block)
  namespace = base
  unless base.const_defined?(name)
    type_of_missing = Module unless type_of_missing
    name = name.uncapsulate('::')
    if name.include?('::')
      namespaces = name.split('::')
      name = namespaces.pop
      namespaces.each do |constant|
        unless namespace.const_defined?(constant)
          match = namespace.const_set(constant, type_of_missing.new)
        end
        namespace = namespace.const_get(constant)
      end
    end
    namespace.const_set(name, value)
  end
  object = namespace.const_get(name)
  raise TypeError, "Expected a #{value.class} but #{namespace}::#{name} is a #{object.class}" if strict && object.class != value.class
  object.tap do |constant|
    constant.send(:class_exec, &block) if block
  end
end
copy_capitalization(str_a, str_b) click to toggle source

Takes two strings and tries to apply the same capitalization from the first string to the second. Supports lower case, upper case and capital case

# File lib/bblib/core/util/string.rb, line 84
def self.copy_capitalization(str_a, str_b)
  str_a = str_a.to_s
  str_b = str_b.to_s
  if str_a.upper?
    str_b.upcase
  elsif str_a.lower?
    str_b.downcase
  elsif str_a.capital?
    str_b.capitalize
  else
    str_b
  end
end
custom_pluralize(num, base, plural = 's', singular = nil) click to toggle source
# File lib/bblib/core/util/pluralization.rb, line 128
def self.custom_pluralize(num, base, plural = 's', singular = nil)
  num == 1 ? "#{base}#{singular}" : "#{base}#{plural}"
end
default_logger() click to toggle source
# File lib/bblib/core/util/logging.rb, line 9
def self.default_logger
  log = ::Logger.new(STDOUT)
  log.level = ::Logger::INFO
  log.formatter = proc do |severity, datetime, progname, msg|
    severity = severity.to_s.to_color(severity) if BBLib.color_logs
    if msg.is_a?(Exception)
      msg = msg.inspect + "\n\t" + msg.backtrace.join("\n\t")
    end
    "#{datetime} [#{severity}] #{msg.to_s.chomp}\n"
  end
  log.datetime_format = '%Y-%m-%d %H:%M:%S'
  log
end
delimited_case(str, delimiter = '_') click to toggle source
# File lib/bblib/core/util/cases.rb, line 44
def self.delimited_case(str, delimiter = '_')
  regx = /[[:space:]]+|\s+|[^\w\d]+|\#{delimiter}+/
  str.split(regx).join(delimiter)
end
drop_symbols(str) click to toggle source

Quickly remove any symbols from a string leaving only alpha-numeric characters and white space.

# File lib/bblib/core/util/string.rb, line 9
def self.drop_symbols(str)
  str.gsub(/[^\w\s\d]|_/, '')
end
enable_logger(enable = true) click to toggle source
# File lib/bblib/core/util/logging.rb, line 28
def self.enable_logger(enable = true)
  @logger_on = enable
end
extract_floats(str, convert: true) click to toggle source

Extracts all integers or decimals from a string into an array.

# File lib/bblib/core/util/string.rb, line 20
def self.extract_floats(str, convert: true)
  BBLib.extract_numbers(str, convert: false).reject { |r| !r.include?('.') }
       .map { |m| convert ? m.to_f : m }
end
extract_integers(str, convert: true) click to toggle source

Extract all integers from a string. Use extract_floats if numbers may contain decimal places.

# File lib/bblib/core/util/string.rb, line 14
def self.extract_integers(str, convert: true)
  BBLib.extract_numbers(str, convert: false).reject { |r| r.include?('.') }
       .map { |m| convert ? m.to_i : m }
end
extract_numbers(str, convert: true, include_inner: true) click to toggle source

Extracts any correctly formed integers or floats from a string

# File lib/bblib/core/util/string.rb, line 29
def self.extract_numbers(str, convert: true, include_inner: true)
  str.scan(include_inner ? EXTRACT_NUMBER_REGEXP : EXTRACT_NUMBER_REGEXP_NO_INNER)
     .map { |f| convert ? (f.include?('.') ? f.to_f : f.to_i) : f }
end
from_roman(str) click to toggle source
# File lib/bblib/core/util/roman.rb, line 30
def self.from_roman(str)
  sp = str.split(' ')
  (0..1000).each do |n|
    num = BBLib.to_roman n
    next if sp.select { |i| i[/#{num}/i] }.empty?
    (0..(sp.length-1)).each do |i|
      sp[i] = sp[i].sub(num, n.to_s) if sp[i].drop_symbols.upcase == num
    end
  end
  sp.join ' '
end
hash_args(*args) click to toggle source

Similar to named_args but also treats String keys as named arguments.

# File lib/bblib/core/util/object.rb, line 61
def self.hash_args(*args)
  args.find_all { |a| a.is_a?(Hash) }.each_with_object({}) { |a, h| h.merge!(a) }
end
hash_path(hash, *paths, multi_path: false, multi_join: false, multi_join_hash: false) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 46
def self.hash_path(hash, *paths, multi_path: false, multi_join: false, multi_join_hash: false)
  tree = TreeHash.new(hash)
  if multi_path
    tree.find_multi(*paths).map  { |r| r.map { |sr| sr.value } }
  elsif multi_join
    tree.find_join(*paths).map { |r| r.map { |sr| sr.value } }
  elsif multi_join_hash
    tree.find_join(*paths).map { |r| r.map { |sr| sr.value } }.to_h
  else
    tree.find(paths).map(&:value)
  end
end
hash_path_copy(hash, *paths) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 73
def self.hash_path_copy(hash, *paths)
  tree = hash.is_a?(TreeHash) ? hash : TreeHash.new(hash)
  tree.copy(*paths)
  hash.replace(tree.value)
end
hash_path_copy_to(from, to, *paths) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 79
def self.hash_path_copy_to(from, to, *paths)
  tree = from.is_a?(TreeHash) ? from : TreeHash.new(from)
  tree.hash_path_copy_to(to, *paths)
end
hash_path_delete(hash, *paths) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 84
def self.hash_path_delete(hash, *paths)
  tree = hash.is_a?(TreeHash) ? hash : TreeHash.new(hash)
  tree.delete(*paths)
  hash.replace(tree.value)
end
hash_path_key_for(hash, value) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 63
def self.hash_path_key_for(hash, value)
  hash.squish.find_all { |_k, v| value.is_a?(Regexp) ? v =~ value : v == value }.to_h.keys
end
hash_path_keys(hash) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 59
def self.hash_path_keys(hash)
  hash.to_tree_hash.absolute_paths
end
hash_path_move(hash, *paths) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 90
def self.hash_path_move(hash, *paths)
  tree = hash.is_a?(TreeHash) ? hash : TreeHash.new(hash)
  tree.move(*paths)
  hash.replace(tree.value)
end
hash_path_move_to(from, to, *paths) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 96
def self.hash_path_move_to(from, to, *paths)
  tree = hash.is_a?(TreeHash) ? hash : TreeHash.new(hash)
  tree.hash_path_copy_to(to, *paths).tap do |res|
    from.replace(tree.value)
    to.replace(res.value)
  end
  to
end
hash_path_set(hash, *paths) click to toggle source
# File lib/bblib/core/hash_path/hash_path.rb, line 67
def self.hash_path_set(hash, *paths)
  tree = hash.is_a?(TreeHash) ? hash : TreeHash.new(hash)
  tree.bridge(*paths)
  hash.replace(tree.value)
end
in_opal?() click to toggle source
# File lib/bblib/core/util/opal.rb, line 2
def self.in_opal?
  RUBY_ENGINE == 'opal'
end
interleave(ary_a, ary_b) click to toggle source

Takes two arrays (can be of different length) and interleaves them like [a, b, a, b…]

# File lib/bblib/core/util/array.rb, line 5
def self.interleave(ary_a, ary_b)
  ary = []
  [ary_a.size, ary_b.size].max.times do |indx|
    ary.push(ary_a[indx]) if indx < ary_a.size
    ary.push(ary_b[indx]) if indx < ary_b.size
  end
  ary
end
is_any?(object, *klasses) click to toggle source

Checks to see if an object is of any of the given classes.

# File lib/bblib/core/util/object.rb, line 13
def self.is_any?(object, *klasses)
  klasses.any? { |klass| object.is_a?(klass) }
end
keep_between(num, min, max) click to toggle source

Used to keep any numeric number between a set of bounds. Passing nil as min or max represents no bounds in that direction. min and max are inclusive to the allowed bounds.

# File lib/bblib/core/util/number.rb, line 5
def self.keep_between(num, min, max)
  num = num.to_f unless num.is_a?(Numeric)
  num = min if min && num < min
  num = max if max && num > max
  num
end
levenshtein_distance(a, b) click to toggle source

A simple rendition of the levenshtein distance algorithm

# File lib/bblib/core/util/matching.rb, line 8
def self.levenshtein_distance(a, b)
  costs = (0..b.length).to_a
  (1..a.length).each do |i|
    costs[0] = i
    nw = i - 1
    (1..b.length).each do |j|
      costs[j], nw = [costs[j] + 1, costs[j-1] + 1, a[i-1] == b[j-1] ? nw : nw + 1].min, costs[j]
    end
  end
  costs[b.length]
end
levenshtein_similarity(a, b) click to toggle source

Calculates a percentage based match using the levenshtein distance algorithm

# File lib/bblib/core/util/matching.rb, line 21
def self.levenshtein_similarity(a, b)
  distance = BBLib.levenshtein_distance a, b
  max = [a.length, b.length].max.to_f
  ((max - distance.to_f) / max) * 100.0
end
log_enabled?() click to toggle source
# File lib/bblib/core/util/logging.rb, line 32
def self.log_enabled?
  @logger_on
end
logger() click to toggle source
# File lib/bblib/core/util/logging.rb, line 5
def self.logger
  @logger ||= default_logger
end
logger=(logger) click to toggle source
# File lib/bblib/core/util/logging.rb, line 23
def self.logger=(logger)
  raise ArgumentError, 'Must be set to a valid logger' unless logger.is_a?(Logger)
  @logger = logger
end
loop_between(num, min, max) click to toggle source

Similar to keep between but when a number exceeds max or is less than min it is looped to the min or max value respectively.

# File lib/bblib/core/util/number.rb, line 14
def self.loop_between(num, min, max)
  num = num.to_f unless num.is_a?(Numeric)
  num = max if min && num < min
  num = min if max && num > max
  num
end
method_case(str) click to toggle source
# File lib/bblib/core/util/cases.rb, line 53
def self.method_case(str)
  str.gsub(/(?<=[^^])([A-Z])(?=[^A-Z\s])/, '_\1').gsub(/[\s\_]+/, '_').snake_case.downcase
end
module_create(name, *args, **opts, &block) click to toggle source
# File lib/bblib/core/util/object.rb, line 118
def self.module_create(name, *args, **opts, &block)
  const_create(name, Module.new(*args), **opts, &block)
end
most_frequent(*args) click to toggle source

Returns the element that occurs the most frequently in an array or

# File lib/bblib/core/util/array.rb, line 15
def self.most_frequent(*args)
  totals = args.each_with_object(Hash.new(0)) { |elem, hash| hash[elem] += 1 }
  max = totals.values.max
  totals.keys.find { |key| totals[key] == max }
end
most_frequent_str(*args, case_insensitive: false) click to toggle source

Returns the most commonly occurring string in an arrray of params. Elements that are not strings are converted to their string representations.

@param [TrueClass, FalseClass] case_insensitive Compare strings case isensitively.

# File lib/bblib/core/util/array.rb, line 25
def self.most_frequent_str(*args, case_insensitive: false)
  most_frequent(*args.map { |arg| case_insensitive ? arg.to_s.downcase : arg.to_s })
end
move_articles(str, position = :front, capitalize: true) click to toggle source

Used to move the position of the articles 'the', 'a' and 'an' in strings for normalization.

# File lib/bblib/core/util/string.rb, line 35
def self.move_articles(str, position = :front, capitalize: true)
  return str unless [:front, :back, :none].include?(position)
  %w(the a an).each do |a|
    starts = str.downcase.start_with?(a + ' ')
    ends = str.downcase.end_with?(' ' + a)
    if starts && position != :front
      if position == :none
        str = str[(a.length + 1)..str.length]
      elsif position == :back
        str = str[(a.length + 1)..str.length] + (!ends ? ", #{capitalize ? a.capitalize : a}" : '')
      end
    end
    next unless ends && position != :back
    if position == :none
      str = str[0..-(a.length + 2)]
    elsif position == :front
      str = (!starts ? "#{capitalize ? a.capitalize : a} " : '') + str[0..-(a.length + 2)]
    end
  end
  str = str.strip.chop while str.strip.end_with?(',')
  str
end
named_args(*args) click to toggle source

Extracts all hash based arguments from an ary of arguments. Only hash pairs with a symbol as the key are returned. Use hash_args if you also want to treat String keys as named arguments.

# File lib/bblib/core/util/object.rb, line 47
def self.named_args(*args)
  args.last.is_a?(Hash) && args.last.keys.all? { |k| k.is_a?(Symbol) } ? args.last : {}
end
named_args!(*args) click to toggle source

Same as standard named_args but removes the named arguments from the array.

# File lib/bblib/core/util/object.rb, line 52
def self.named_args!(*args)
  if args.last.is_a?(Hash) && args.last.keys.all? { |k| k.is_a?(Symbol) }
    args.delete_at(-1)
  else
    {}
  end
end
namespace_of(klass) click to toggle source

Returns the encapsulating object space of a given class. Ex: For a class called BBLib::String, this method will return BBLib as the namespace. Ex2: For a class BBLib::String::Char, this method will return BBLib::String as the namespace.

# File lib/bblib/core/util/object.rb, line 76
def self.namespace_of(klass)
  split = klass.to_s.split('::')
  return klass if split.size == 1
  Object.const_get(split[0..-2].join('::'))
end
number_spelled_out(number, range = 0, include_and: true) click to toggle source

TODO: Support floats eventually?

# File lib/bblib/core/util/number.rb, line 61
def self.number_spelled_out(number, range = 0, include_and: true)
  number = number.to_i
  negative = number.negative?
  number = number * -1 if negative
  return 'zero' if number.zero?
  str = []
  three_digit = number > 999 ? number.to_s[-3..-1].to_i : number
  case three_digit
  when 1..19
    str << NUMBER_WORDS[:special][three_digit]
  when 20..99
    str << NUMBER_WORDS[:double_range][three_digit.to_s[-2].to_i]
    str << NUMBER_WORDS[:special][three_digit.to_s[-1].to_i]
  when 100..999
    str << NUMBER_WORDS[:special][three_digit.to_s[0].to_i]
    str << 'hundred'
    str << 'and' if include_and && !three_digit.to_s.end_with?('00')
    if three_digit.to_s[-2].to_i == 1
      str << NUMBER_WORDS[:special][three_digit.to_s[-2..-1].to_i]
    else
      str << NUMBER_WORDS[:double_range][three_digit.to_s[-2].to_i]
      str << NUMBER_WORDS[:special][three_digit.to_s[-1].to_i]
    end
  end
  str << NUMBER_WORDS[:ranges][range] unless str.compact.empty?
  (negative ? 'negative ' : '') +
  ((number.to_s.size > 3 ? "#{number_spelled_out(number.to_s[0..-4].to_i, range + 1)} " : '') +
  str.compact.join(' ')).gsub(/\s+/, ' ')
end
numeric_similarity(a, b) click to toggle source

Extracts all numbers from two strings and compares them and generates a percentage of match. Percentage calculations here need to be weighted better…TODO

# File lib/bblib/core/util/matching.rb, line 60
def self.numeric_similarity(a, b)
  a = a.extract_numbers
  b = b.extract_numbers
  return 100.0 if a.empty? && b.empty? || a == b
  matches = []
  (0..[a.size, b.size].max-1).each do |i|
    matches << 1.0 / ([a[i].to_f, b[i].to_f].max - [a[i].to_f, b[i].to_f].min + 1.0)
  end
  (matches.inject { |sum, m| sum + m } / matches.size.to_f) * 100.0
end
parse_duration(str, output: :sec, min_interval: :sec) click to toggle source

Parses known time based patterns out of a string to construct a numeric duration.

# File lib/bblib/core/util/time.rb, line 3
def self.parse_duration(str, output: :sec, min_interval: :sec)
  msecs = 0.0

  # Parse time expressions such as 04:05.
  # The argument min_interval controls what time interval the final number represents
  str.scan(/\d+\:[\d+\:]+\d+/).each do |e|
    keys = TIME_EXPS.keys
    position = keys.index(min_interval)
    e.split(':').reverse.each do |sec|
      key = keys[position]
      msecs+= sec.to_f * TIME_EXPS[key][:mult]
      position+=1
    end
  end

  # Parse expressions such as '1m' or '1 min'
  TIME_EXPS.each do |_k, v|
    v[:exp].each do |e|
      numbers = str.downcase.scan(/(?=\w|\D|\A)\d*\.?\d+[[:space:]]*#{e}(?=\W|\d|\z)/i)
      numbers.each do |n|
        msecs+= n.to_f * v[:mult]
      end
    end
  end
  msecs / TIME_EXPS[output][:mult]
end
parse_file_size(str, output: :byte) click to toggle source

A file size parser for strings. Extracts any known patterns for file sizes.

# File lib/bblib/core/util/file.rb, line 58
def self.parse_file_size(str, output: :byte)
  output = FILE_SIZES.keys.find { |fs| fs == output || FILE_SIZES[fs][:exp].include?(output.to_s.downcase) } || :byte
  bytes = 0.0
  FILE_SIZES.each do |_k, v|
    v[:exp].each do |exp|
      str.scan(/(?=\w|\D|^)\d*\.?\d+\s*#{exp}s?(?=\W|\d|$)/i)
         .each { |num| bytes += num.to_f * v[:mult] }
    end
  end
  bytes / FILE_SIZES[output][:mult]
end
pathify(*strings) click to toggle source

Takes one or more strings and normalizes slashes to create a consistent file path Useful when concating two strings that when you don't know if one or both will end or begin with a slash

# File lib/bblib/core/util/file.rb, line 5
def self.pathify(*strings)
  (strings.first.start_with?('/', '\\') ? strings.first.scan(/^[\/\\]{1,2}/).first : '') + strings.map(&:to_s).msplit('/', '\\').map(&:strip).join('/')
end
pattern_render(text, context = {}) click to toggle source

Pattern render takes (by default) a mustache style template and then uses a context (either a Hash or Object) to then interpolate in placeholders. The default pattern looks for {{method_name}} within the string but can be customized to a different pattern by setting the pattern named argument.

# File lib/bblib/core/util/string.rb, line 102
def self.pattern_render(text, context = {})
  raise ArgumentError, "Expected text argument to be a String, got a #{text.class}" unless text.is_a?(String)
  # TODO Make patterns customizable
  pattern       = /\{{2}.*?\}{2}/
  field_pattern = /(?<=^\{{2}).*(?=\}{2})/
  txt           = text.dup
  txt.scan(pattern).each do |match|
    field = match.scan(field_pattern).first
    next unless field
    value = case context
    when Hash
      context.hpath(field).first
    else
      context.send(field) if context.respond_to?(field)
    end.to_s
    txt.sub!(match, value)
  end
  txt
end
phrase_similarity(a, b) click to toggle source

Calculates a percentage based match between two strings based on the similarity of word matches.

# File lib/bblib/core/util/matching.rb, line 46
def self.phrase_similarity(a, b)
  temp = b.drop_symbols.split ' '
  matches = 0
  a.drop_symbols.split(' ').each do |w|
    if temp.include? w
      matches+=1
      temp.delete_at temp.find_index w
    end
  end
  (matches.to_f / [a.split(' ').size, b.split(' ').size].max.to_f) * 100.0
end
plural_string(num, string) click to toggle source
# File lib/bblib/core/util/pluralization.rb, line 132
def self.plural_string(num, string)
  "#{num} #{pluralize(string, num)}"
end
pluralize(string, num = 2) click to toggle source
# File lib/bblib/core/util/pluralization.rb, line 91
def self.pluralize(string, num = 2)
  full_string = string.to_s
  string = string.split(/\s+/).last
  sym = string.to_s.downcase.to_sym
  if plural = SPECIAL_PLURALS[sym]
    result = num == 1 ? string : plural
  else
    if string.end_with?(*%w{ch z s x o})
      result = num == 1 ? string : (string + 'es')
    elsif string =~ /[^aeiou]y$/i
      result = num == 1 ? string : string.sub(/y$/i, 'ies')
    else
      result = num == 1 ? string : (string + 's')
    end
  end
  full_string.sub(/#{Regexp.escape(string)}$/, copy_capitalization(string, result).to_s)
end
qwerty_distance(a, b) click to toggle source

A simple character distance calculator that uses qwerty key positions to determine how similar two strings are. May be useful for typo detection.

# File lib/bblib/core/util/matching.rb, line 73
def self.qwerty_distance(a, b)
  a = a.downcase.strip
  b = b.downcase.strip
  if a.length <= b.length
    t = a
    a = b
    b = t
  end
  qwerty = {
    1 => %w(1 2 3 4 5 6 7 8 9 0),
    2 => %w(q w e r t y u i o p),
    3 => %w(a s d f g h j k l),
    4 => %w(z x c v b n m)
  }
  count = 0
  offset = 0
  a.chars.each do |c|
    if b.length <= count
      offset+=10
    else
      ai = qwerty.keys.find { |f| qwerty[f].include? c }.to_i
      bi = qwerty.keys.find { |f| qwerty[f].include? b.chars[count] }.to_i
      offset+= (ai - bi).abs
      offset+= (qwerty[ai].index(c) - qwerty[bi].index(b.chars[count])).abs
    end
    count+=1
  end
  offset
end
recursive_send(obj, *methods) click to toggle source

Send a chain of methods to an object and each result of the previous method.

# File lib/bblib/core/util/object.rb, line 66
def self.recursive_send(obj, *methods)
  methods.each do |args|
    obj = obj.send(*args)
  end
  obj
end
root_namespace_of(klass) click to toggle source

Returns the root namespace of a given class if it is nested.

# File lib/bblib/core/util/object.rb, line 83
def self.root_namespace_of(klass)
  Object.const_get(klass.to_s.gsub(/::.*/, ''))
end
scan_dir(path, *filters, recursive: false, files: true, dirs: true, exclude: [], filter_base: true) { |item| ... } click to toggle source

Scan for files and directories. Can be set to be recursive and can also have filters applied. @param [String] path The directory to scan files from. @param [String…, Regexp…] filters A list of filters to apply. Can be regular expressions or strings.

Strings with a * are treated as regular expressions with a .*. If no filters are passed, all files/dirs are returned.

@param [Boolean] recursive When true scan will recursively search directories @param [Boolean] files If true, paths to files matching the filter will be returned. @param [Boolean] dirs If true, paths to dirs matching the filter will be returned. @param [Array] exclude Can be an array of regular expressions or strings that should be ignored when scanning. * in a string is expanded into .*, but all other characters are literal.

# File lib/bblib/core/util/file.rb, line 17
def self.scan_dir(path, *filters, recursive: false, files: true, dirs: true, exclude: [], filter_base: true, &block)
  return [] unless Dir.exist?(path)
  filters = filters.map { |filter| filter.is_a?(Regexp) ? filter : /^#{Regexp.quote(filter).gsub('\\*', '.*')}$/ }
  exclude = exclude ? [exclude].flatten.map { |exp| exp.is_a?(Regexp) ? exp : /^#{Regexp.quote(exp).gsub('\\*', '.*')}$/ } : []
  Dir.foreach(path).flat_map do |item|
    next if item =~ /^\.{1,2}$/ || (!exclude.empty? && exclude.any? { |exp| item =~ exp })
    item = "#{path}/#{item}".gsub('\\', '/')
    if File.file?(item)
      if files && (filters.empty? || filters.any? { |filter| item =~ filter || filter_base && item.file_name =~ filter })
        block_given? ? yield(item) : item
      end
    elsif File.directory?(item)
      recur = recursive ? scan_dir(item, *filters, recursive: recursive, exclude: exclude, files: files, dirs: dirs, &block) : []
      if dirs && (filters.empty? || filters.any? { |filter| item =~ filter || filter_base && item.file_name =~ filter })
        (block_given? ? yield(item) : [item] + recur)
      elsif recursive
        recur
      end
    end
  end.compact
end
scan_dirs(path, *filters, recursive: false, exclude: [], filter_base: true, &block) click to toggle source

Uses BBLib.scan_dir but returns only directories.

# File lib/bblib/core/util/file.rb, line 45
def self.scan_dirs(path, *filters, recursive: false, exclude: [], filter_base: true, &block)
  scan_dir(path, *filters, recursive: recursive, files: false, exclude: exclude, filter_base: filter_base, &block)
end
scan_files(path, *filters, recursive: false, exclude: [], filter_base: true, &block) click to toggle source

Uses BBLib.scan_dir but returns only files

# File lib/bblib/core/util/file.rb, line 40
def self.scan_files(path, *filters, recursive: false, exclude: [], filter_base: true, &block)
  scan_dir(path, *filters, recursive: recursive, dirs: false, exclude: exclude, filter_base: filter_base, &block)
end
singularize(string) click to toggle source
# File lib/bblib/core/util/pluralization.rb, line 109
def self.singularize(string)
  full_string = string.to_s
  string = string.split(/\s+/).last
  sym = string.to_s.downcase.to_sym
  sym = string.to_s.downcase.to_sym
  if singular = SPECIAL_PLURALS.find { |k, v| v == sym }
    result = singular.first
  elsif string.downcase.end_with?(*%w{oes ches zes ses xes})
    result = string.sub(/es$/i, '')
  elsif string =~ /ies$/i
    result = string.sub(/ies$/i, 'y')
  elsif string =~ /s$/i && !(string =~ /s{2}$/i)
    result = string.sub(/s$/i, '')
  else
    result = string
  end
  full_string.sub(/#{Regexp.escape(string)}$/, copy_capitalization(string, result).to_s)
end
snake_case(str) click to toggle source
# File lib/bblib/core/util/cases.rb, line 49
def self.snake_case(str)
  BBLib.delimited_case(str, '_')
end
spinal_case(str) click to toggle source
# File lib/bblib/core/util/cases.rb, line 61
def self.spinal_case(str)
  BBLib.delimited_case str, '-'
end
start_case(str, first_only: false) click to toggle source
# File lib/bblib/core/util/cases.rb, line 23
def self.start_case(str, first_only: false)
  regx = /[[:space:]]+|\-|\_|\"|\'|\(|\)|\[|\]|\{|\}|\#/
  spacing = str.scan(regx).to_a
  words = str.split(regx).map do |word|
    if first_only
      word[0] = word[0].upcase
      word
    else
      word.capitalize
    end
  end
  words.interleave(spacing).join
end
string_to_file(str, path, mkpath: true, mode: 'a') click to toggle source

Shorthand method to write a string to disk. By default the path is created if it doesn't exist. Set mode to w to truncate file or leave at a to append.

# File lib/bblib/core/util/file.rb, line 52
def self.string_to_file(str, path, mkpath: true, mode: 'a')
  FileUtils.mkpath(File.dirname(path)) if mkpath && !Dir.exist?(path)
  File.write(path, str.to_s, mode: mode)
end
string_to_roman(str) click to toggle source
# File lib/bblib/core/util/roman.rb, line 19
def self.string_to_roman(str)
  sp = str.split ' '
  sp.map do |s|
    if s.drop_symbols.to_i.to_s == s.drop_symbols && !(s =~ /\d+\.\d+/)
      s.sub(s.scan(/\d+/).first.to_s, BBLib.to_roman(s.to_i))
    else
      s
    end
  end.join(' ')
end
title_case(str, first_only: true) click to toggle source
# File lib/bblib/core/util/cases.rb, line 2
def self.title_case(str, first_only: true)
  str = str.to_s unless str.is_a?(String)
  ignoreables = %w(a an the on upon and but or in with to)
  regx = /\s+|\-|\_|(?<=[\w\d])\.(?=[\w\d])|(?<=\W|^)\"(?=\w|$)|(?<=\W|^)\'(?=\w|$)|\(|\)|\[|\]|\{|\}|\#/
  spacing = str.scan(regx).to_a
  words = str.split(regx).map do |word|
    if ignoreables.include?(word.downcase)
      word.downcase
    elsif first_only
      word.to_s.slice(0,1).to_s.upcase + word.to_s[1..-1].to_s
    else
      word.capitalize
    end
  end
  # Always cap the first word
  words[0] = words.first.to_s.slice(0,1).to_s.upcase + words.first.to_s[1..-1].to_s
  combined = words.interleave(spacing).join
  combined.scan(/(?<=\.)\w(?=\.)/).each { |part| combined.sub!(".#{part}.", ".#{part}.".upcase) }
  combined
end
to_duration(num, input: :sec, stop: :milli, style: :medium) click to toggle source

Turns a numeric input into a time string.

# File lib/bblib/core/util/time.rb, line 31
def self.to_duration(num, input: :sec, stop: :milli, style: :medium)
  return nil unless num.is_a?(Numeric)
  return '0' if num.zero?
  style = :medium unless [:long, :medium, :short].include?(style)
  expression = []
  n = num * TIME_EXPS[input.to_sym][:mult]
  done = false
  TIME_EXPS.reverse.each do |k, v|
    next if done
    done = true if k == stop
    div = n / v[:mult]
    next unless div >= 1
    val = (done ? div.round : div.floor)
    expression << "#{val}#{v[:styles][style]}#{val > 1 && style != :short ? 's' : nil}"
    n -= val.to_f * v[:mult]
  end
  expression.join(' ')
end
to_file_size(num, input: :byte, stop: :byte, style: :short) click to toggle source

Takes an integer or float and converts it into a string that represents

a file size (e.g. "5 MB 156 kB")

@param [Integer, Float] num The number of bytes to convert to a file size string. @param [Symbol] input Sets the value of the input. Default is byte. @param [Symbol] stop Sets a minimum file size to display.

e.g. If stop is set to :megabyte, :kilobyte and below will be truncated.

@param [Symbol] style The out style, Current options are :short and :long

# File lib/bblib/core/util/file.rb, line 77
def self.to_file_size(num, input: :byte, stop: :byte, style: :short)
  return nil unless num.is_a?(Numeric)
  return '0' if num.zero?
  style = :short unless [:long, :short].include?(style)
  expression = []
  n = num * FILE_SIZES[input.to_sym][:mult]
  done = false
  FILE_SIZES.reverse.each do |k, v|
    next if done
    done = true if k == stop
    div = n / v[:mult]
    next unless div >= 1
    val = (done ? div.round : div.floor)
    expression << "#{val}#{v[:styles][style]}#{val > 1 && style != :short ? 's' : nil}"
    n -= val.to_f * v[:mult]
  end
  expression.join(' ')
end
to_hash(obj) click to toggle source

Takes any type of object and converts it into a hash based on its instance variables.

# File lib/bblib/core/util/object.rb, line 19
def self.to_hash(obj)
  return { obj => nil } if obj.instance_variables.empty?
  hash = {}
  obj.instance_variables.each do |var|
    value = obj.instance_variable_get(var)
    if value.is_a?(Array)
      hash[var.to_s.delete('@')] = value.map { |v| v.respond_to?(:obj_to_hash) && !v.instance_variables.empty? ? v.obj_to_hash : v }
    elsif value.is_a?(Hash)
      begin
        unless hash[var.to_s.delete('@')].is_a?(Hash) then hash[var.to_s.delete('@')] = {} end
      rescue
        hash[var.to_s.delete('@')] = {}
      end
      value.each do |k, v|
        hash[var.to_s.delete('@')][k.to_s.delete('@')] = v.respond_to?(:obj_to_hash) && !v.instance_variables.empty? ? v.obj_to_hash : v
      end
    elsif value.respond_to?(:obj_to_hash) && !value.instance_variables.empty?
      hash[var.to_s.delete('@')] = value.obj_to_hash
    else
      hash[var.to_s.delete('@')] = value
    end
  end
  hash
end
to_nearest_duration(num, input: :sec, style: :medium) click to toggle source
# File lib/bblib/core/util/time.rb, line 50
def self.to_nearest_duration(num, input: :sec, style: :medium)
  n = num * TIME_EXPS[input.to_sym][:mult]
  stop = nil
  TIME_EXPS.each do |k, v|
    stop = k if v[:mult] <= n
  end
  stop = :year unless stop
  to_duration(num, input: input, style: style, stop: stop)
end
to_roman(num) click to toggle source

Converts any integer up to 1000 to a roman numeral

# File lib/bblib/core/util/roman.rb, line 7
def self.to_roman(num)
  return num.to_s if num > 1000
  numeral = ''
  ROMAN_NUMERALS.each do |n, r|
    while num >= n
      num -= n
      numeral += r
    end
  end
  numeral
end
train_case(str) click to toggle source
# File lib/bblib/core/util/cases.rb, line 65
def self.train_case(str)
  BBLib.spinal_case(BBLib.start_case(str))
end