class String

Chronify methods for strings

Template coloring

String to symbol conversion

Tag and search highlighting

Handling of search and regex strings

Handling of @tags in strings

String helpers

String truncation

URL linking and formatting

Public Instance Methods

add_at() click to toggle source

Add @ prefix to string if needed, maintains +/- prefix

@return [String] @string

# File lib/doing/string/tags.rb, line 11
def add_at
  strip.sub(/^([+-]*)@?/, '\1@')
end
add_tags(tags, remove: false) click to toggle source

@brief Adds tags to a string

@param tags [String or Array] List of tags to add. @ symbol optional @param remove [Boolean] remove tags instead of adding

@return [String] the tagged string

# File lib/doing/string/tags.rb, line 48
def add_tags(tags, remove: false)
  title = dup
  tags = tags.to_tags
  tags.each { |tag| title.tag!(tag, remove: remove) }
  title
end
add_tags!(tags, remove: false) click to toggle source

@see add_tags

# File lib/doing/string/tags.rb, line 56
def add_tags!(tags, remove: false)
  replace add_tags(tags, remove: remove)
end
cap_first() click to toggle source

Capitalize on the first character on string

@return Capitalized string

# File lib/doing/string/transform.rb, line 102
def cap_first
  sub(/^\w/) do |m|
    m.upcase
  end
end
chronify(**options) click to toggle source

Converts input string into a Time object when input takes on the following formats: - interval format e.g. '1d2h30m', '45m' etc. - a semantic phrase e.g. 'yesterday 5:30pm' - a strftime e.g. '2016-03-15 15:32:04 PDT'

@param options Additional options

@option options :future [Boolean] assume future date (default: false)

@option options :guess [Symbol] :begin or :end to assume beginning or end of arbitrary time range

@return [DateTime] result

# File lib/doing/chronify/string.rb, line 27
def chronify(**options)
  now = Time.now
  raise Errors::InvalidTimeExpression, "Invalid time expression #{inspect}" if to_s.strip == ''

  secs_ago = if match(/^(\d+)$/)
               # plain number, assume minutes
               Regexp.last_match(1).to_i * 60
             elsif (m = match(/^(?:(?<day>\d+)d)? *(?:(?<hour>\d+)h)? *(?:(?<min>\d+)m)?$/i))
               # day/hour/minute format e.g. 1d2h30m
               [[m['day'], 24 * 3600],
                [m['hour'], 3600],
                [m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+)
             end

  if secs_ago
    res = now - secs_ago
    Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res} (#{secs_ago} seconds ago)))
  else
    date_string = dup
    date_string = 'today' if date_string.match(Types::REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
    date_string = "#{options[:context].to_s} #{date_string}" if date_string =~ Types::REGEX_TIME && options[:context]

    res = Chronic.parse(date_string, {
                          guess: options.fetch(:guess, :begin),
                          context: options.fetch(:future, false) ? :future : :past,
                          ambiguous_time_range: 8
                        })

    Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res}))
  end

  res
end
chronify_qty() click to toggle source

Converts simple strings into seconds that can be added to a Time object

Input string can be HH:MM or XX[[XXhm]] (1d2h30m, 45m, 1.5d, 1h20m, etc.)

@return [Integer] seconds

# File lib/doing/chronify/string.rb, line 70
def chronify_qty
  minutes = 0
  case self.strip
  when /^(\d+):(\d\d)$/
    minutes += Regexp.last_match(1).to_i * 60
    minutes += Regexp.last_match(2).to_i
  when /^(\d+(?:\.\d+)?)([hmd])?/
    scan(/(\d+(?:\.\d+)?)([hmd])?/).each do |m|
      amt = m[0]
      type = m[1].nil? ? 'm' : m[1]

      minutes += case type.downcase
                 when 'm'
                   amt.to_i
                 when 'h'
                   (amt.to_f * 60).round
                 when 'd'
                   (amt.to_f * 60 * 24).round
                 else
                   0
                 end
    end
  end
  minutes * 60
end
clean_unlinked_urls() click to toggle source

Clean up unlinked <urls>

# File lib/doing/string/url.rb, line 71
def clean_unlinked_urls
  gsub(/<(\w+:.*?)>/) do |match|
    m = Regexp.last_match
    if m[1] =~ /<a href/
      match
    else
      %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
    end
  end
end
compress() click to toggle source

Compress multiple spaces to single space

# File lib/doing/string/transform.rb, line 9
def compress
  gsub(/ +/, ' ').strip
end
compress!() click to toggle source
# File lib/doing/string/transform.rb, line 13
def compress!
  replace compress
end
dedup_tags() click to toggle source

Remove duplicate tags, leaving only first occurrence

@return Deduplicated string

# File lib/doing/string/tags.rb, line 148
def dedup_tags
  title = dup
  tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
  tags.each do |tag|
    found = false
    title.gsub!(/( |^)#{Regexp.escape(tag[1])}(\([^)]+\))?(?= |$)/) do |m|
      if found
        ''
      else
        found = true
        m
      end
    end
  end
  title
end
dedup_tags!() click to toggle source

@see dedup_tags

# File lib/doing/string/tags.rb, line 166
def dedup_tags!
  replace dedup_tags
end
expand_date_tags(additional_tags = nil) click to toggle source

Convert (chronify) natural language dates within configured date tags (tags whose value is expected to be a date). Modifies string in place.

@param additional_tags [Array] An array of additional tags to consider date_tags

# File lib/doing/chronify/string.rb, line 130
def expand_date_tags(additional_tags = nil)
  iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/

  watch_tags = [
    'start(?:ed)?',
    'beg[ia]n',
    'done',
    'finished',
    'completed?',
    'waiting',
    'defer(?:red)?'
  ]

  if additional_tags
    date_tags = additional_tags
    date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
    date_tags.map! do |tag|
      tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
    end
    watch_tags.concat(date_tags).uniq!
  end

  done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i

  gsub!(done_rx) do
    m = Regexp.last_match
    t = m['tag']
    d = m['date']
    future = t =~ /^(done|complete)/ ? false : true
    parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
    parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
  end
end
good?() click to toggle source

Tests if object is nil or empty

@return [Boolean] true if object is defined and has content

# File lib/doing/good.rb, line 24
def good?
  !strip.empty?
end
highlight_errors() click to toggle source
# File lib/helpers/threaded_tests_string.rb, line 5
def highlight_errors
  cols = `tput cols`.strip.to_i

  string = dup

  errs = string.scan(/(?<==\n)(?:Failure|Error):.*?(?=\n=+)/m)

  errs.map! do |error|
    err = error.dup

    err.gsub!(%r{^(/.*?/)([^/:]+):(\d+):in (.*?)$}) do
      m = Regexp.last_match
      "#{m[1].white}#{m[2].bold.white}:#{m[3].yellow}:in #{m[4].cyan}"
    end
    err.gsub!(/(Failure|Error): (.*?)\((.*?)\):\n  (.*?)(?=\n)/m) do
      m = Regexp.last_match
      [
        m[1].bold.boldbgred.white,
        m[3].bold.boldbgcyan.white,
        m[2].bold.boldbgyellow.black,
        " #{m[4]} ".bold.boldbgwhite.black.reset
      ].join(':'.boldblack.boldbgblack.reset)
    end
    err.gsub!(/(<.*?>) (was expected to) (.*?)\n( *<.*?>)./m) do
      m = Regexp.last_match
      "#{m[1].bold.green} #{m[2].white} #{m[3].boldwhite.boldbgred.reset}\n#{m[4].bold.white}"
    end
    err.gsub!(/(Finished in) ([\d.]+) (seconds)/) do
      m = Regexp.last_match
      "#{m[1].green} #{m[2].bold.white} #{m[3].green}"
    end
    err.gsub!(/(\d+) (failures)/) do
      m = Regexp.last_match
      "#{m[1].bold.red} #{m[2].red}"
    end
    err.gsub!(/100% passed/) do |m|
      m.bold.green
    end

    err
  end

  errs.join("\n#{('=' * cols).blue}\n")
end
highlight_search!(search, distance: nil, negate: false, case_type: nil) click to toggle source
# File lib/doing/string/highlight.rb, line 32
def highlight_search!(search, distance: nil, negate: false, case_type: nil)
  replace highlight_search(search, distance: distance, negate: negate, case_type: case_type)
end
highlight_tags(color = 'yellow', last_color: nil) click to toggle source

Colorize @tags with ANSI escapes

@param color [String] color (see #Color)

@return [String] string with @tags highlighted

# File lib/doing/string/highlight.rb, line 18
def highlight_tags(color = 'yellow', last_color: nil)
  unless last_color
    escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
    color = color.split(' ') unless color.is_a?(Array)
    tag_color = color.each_with_object([]) { |c, arr| arr << Doing::Color.send(c) }.join('')
    last_color = if escapes.good?
                   (escapes.count > 1 ? escapes[-2..-1] : [escapes[-1]]).map { |v| v[0] }.join('')
                 else
                   Doing::Color.default
                 end
  end
  gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
end
highlight_tags!(color = 'yellow', last_color: nil) click to toggle source

@param (see highlight_tags)

# File lib/doing/string/highlight.rb, line 7
def highlight_tags!(color = 'yellow', last_color: nil)
  replace highlight_tags(color)
end
ignore?() click to toggle source

Test if line should be ignored

@return [Boolean] line is empty or comment

# File lib/doing/string/query.rb, line 24
def ignore?
  line = self
  line =~ /^#/ || line =~ /^\s*$/
end
ignore_case(search, case_type) click to toggle source

Determine whether case should be ignored for string

@param search The search string @param case_type The case type, :smart, :sensitive, :ignore

@return [Boolean] true if case should be ignored

# File lib/doing/string/query.rb, line 15
def ignore_case(search, case_type)
  (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
end
is_range?() click to toggle source
# File lib/doing/chronify/string.rb, line 164
def is_range?
  self =~ / (to|through|thru|(un)?til|-+) /
end
last_color() click to toggle source

Returns the last escape sequence from a string.

Actually returns all escape codes, with the assumption that the result of inserting them will generate the same color as was set at the end of the string. Because you can send modifiers like dark and bold separate from color codes, only using the last code may not render the same style.

@return [String] All escape codes in string

# File lib/doing/string/highlight.rb, line 74
def last_color
  scan(/\e\[[\d;]+m/).join('')
end
ltrunc(max) click to toggle source
# File lib/doing/completion/string.rb, line 6
def ltrunc(max)
  if length > max
    sub(/^.*?(.{#{max - 3}})$/, '...\1')
  else
    self
  end
end
ltrunc!(max) click to toggle source
# File lib/doing/completion/string.rb, line 14
def ltrunc!(max)
  replace ltrunc(max)
end
normalize_age(default = :newest) click to toggle source

Convert an age string to a qualified type

@return [Symbol] :oldest or :newest

# File lib/doing/normalize.rb, line 34
def normalize_age(default = :newest)
  case self
  when /^o/i
    :oldest
  when /^n/i
    :newest
  else
    default
  end
end
normalize_age!(default = :newest) click to toggle source

@see normalize_age

# File lib/doing/normalize.rb, line 46
def normalize_age!(default = :newest)
  replace normalize_age(default)
end
normalize_bool(default = :and) click to toggle source

Convert a boolean string to a symbol

@return Symbol :and, :or, or :not

# File lib/doing/normalize.rb, line 98
def normalize_bool(default = :and)
  case self
  when /(and|all)/i
    :and
  when /(any|or)/i
    :or
  when /(not|none)/i
    :not
  when /^p/i
    :pattern
  else
    default.is_a?(Symbol) ? default : default.normalize_bool
  end
end
normalize_bool!(default = :and) click to toggle source

@see normalize_bool

# File lib/doing/normalize.rb, line 114
def normalize_bool!(default = :and)
  replace normalize_bool(default)
end
normalize_case(default = :smart) click to toggle source

Convert a case sensitivity string to a symbol

@return Symbol :smart, :sensitive, :ignore

# File lib/doing/normalize.rb, line 75
def normalize_case(default = :smart)
  case self
  when /^(c|sens)/i
    :sensitive
  when /^i/i
    :ignore
  when /^s/i
    :smart
  else
    default.is_a?(Symbol) ? default : default.normalize_case
  end
end
normalize_case!(default = :smart) click to toggle source

@see normalize_case

# File lib/doing/normalize.rb, line 89
def normalize_case!(default = :smart)
  replace normalize_case(default)
end
normalize_color() click to toggle source

Normalize a color name, removing underscores, replacing “bright” with “bold”, and converting bgbold to boldbg

@return [String] Normalized color name

# File lib/doing/colors.rb, line 116
def normalize_color
  gsub(/_/, '').sub(/bright/i, 'bold').sub(/bgbold/, 'boldbg')
end
normalize_matching(default = :pattern) click to toggle source

Convert a matching configuration string to a symbol

@param default [Symbol] the default matching type to return if the string doesn't match a known symbol @return Symbol :fuzzy, :pattern, :exact

# File lib/doing/normalize.rb, line 126
def normalize_matching(default = :pattern)
  case self
  when /^f/i
    :fuzzy
  when /^p/i
    :pattern
  when /^e/i
    :exact
  else
    default.is_a?(Symbol) ? default : default.normalize_matching
  end
end
normalize_matching!(default = :pattern) click to toggle source

@see normalize_matching

# File lib/doing/normalize.rb, line 140
def normalize_matching!(default = :pattern)
  replace normalize_bool(default)
end
normalize_order(default = :asc) click to toggle source
# File lib/doing/normalize.rb, line 59
def normalize_order(default = :asc)
  case self
  when /^a/i
    :asc
  when /^d/i
    :desc
  else
    default
  end
end
normalize_order!(default = :asc) click to toggle source

Convert a sort order string to a qualified type

@return [Symbol] :asc or :desc

# File lib/doing/normalize.rb, line 55
def normalize_order!(default = :asc)
  replace normalize_order(default)
end
normalize_tag_sort(default = :name) click to toggle source

Convert tag sort string to a qualified type

@return [Symbol] :name or :time

# File lib/doing/normalize.rb, line 13
def normalize_tag_sort(default = :name)
  case self
  when /^n/i
    :name
  when /^t/i
    :time
  else
    default
  end
end
normalize_tag_sort!(default = :name) click to toggle source

@see normalize_tag_sort

# File lib/doing/normalize.rb, line 25
def normalize_tag_sort!(default = :name)
  replace normalize_tag_sort(default)
end
normalize_trigger() click to toggle source

Adds ?: to any parentheticals in a regular expression to avoid match groups

@return [String] modified regular expression

# File lib/doing/normalize.rb, line 150
def normalize_trigger
  gsub(/\((?!\?:)/, '(?:').downcase
end
normalize_trigger!() click to toggle source

@see normalize_trigger

# File lib/doing/normalize.rb, line 155
def normalize_trigger!
  replace normalize_trigger
end
remove_at() click to toggle source

Removes @ prefix if needed, maintains +/- prefix

@return [String] string without @ prefix

# File lib/doing/string/tags.rb, line 20
def remove_at
  strip.sub(/^([+-]*)@?/, '\1')
end
replace_qualified_urls(**options) click to toggle source

Replace qualified urls

# File lib/doing/string/url.rb, line 46
def replace_qualified_urls(**options)
  fmt = options.fetch(:format, :html)
  gsub(%r{(?mi)(?x:
  (?<!["'\[(\\])
  (?<protocol>(?:http|https)://)
  (?<domain>[\w\-]+(?:\.[\w\-]+)+)
  (?<path>[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
  )}) do |_match|
    m = Regexp.last_match
    url = "#{m['domain']}#{m['path']}"
    proto = m['protocol'].nil? ? 'http://' : m['protocol']
    case fmt
    when :terminal
      TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
    when :html
      %(<a href="#{proto}#{url}" title="Link to #{m['domain']}">[#{url}]</a>)
    when :markdown
      "[#{url}](#{proto}#{url})"
    else
      m[0]
    end
  end
end
rx?() click to toggle source

Determines if receiver is surrounded by slashes or starts with single quote

@return [Boolean] True if regex, False otherwise.

# File lib/doing/string/query.rb, line 34
def rx?
  self =~ %r{(^/.*?/$|^')}
end
sanitize() click to toggle source
# File lib/doing/completion/zsh_completion.rb, line 6
def sanitize
  gsub(/'/, '\\\'').gsub(/\[/, '(').gsub(/\]/, ')')
end
set_type(kind = nil) click to toggle source

Convert a string value to an appropriate type. If kind is not specified, '[one, two]' becomes an Array, '1' becomes Integer, '1.5' becomes Float, 'true' or 'yes' becomes TrueClass, 'false' or 'no' becomes FalseClass.

@param kind [String] specify string, array, integer, float, symbol, or boolean (falls back to string if value is not recognized) @return Converted object type

# File lib/doing/string/transform.rb, line 130
def set_type(kind = nil)
  if kind
    case kind.to_s
    when /^a/i
      gsub(/^\[ *| *\]$/, '').split(/ *, */)
    when /^i/i
      to_i
    when /^(fa|tr)/i
      to_bool
    when /^f/i
      to_f
    when /^sy/i
      sub(/^:/, '').to_sym
    when /^b/i
      self =~ /^(true|yes)$/ ? true : false
    else
      to_s
    end
  else
    case self
    when /(^\[.*?\]$| *, *)/
      gsub(/^\[ *| *\]$/, '').split(/ *, */)
    when /^[0-9]+$/
      to_i
    when /^[0-9]+\.[0-9]+$/
      to_f
    when /^:\w+/
      sub(/^:/, '').to_sym
    when /^(true|yes)$/i
      true
    when /^(false|no)$/i
      false
    else
      to_s
    end
  end
end
short_desc() click to toggle source
# File lib/doing/completion/string.rb, line 2
def short_desc
  split(/[,.]/)[0].sub(/ \(.*?\)?$/, '').strip
end
simple_wrap(width) click to toggle source
# File lib/doing/string/transform.rb, line 17
def simple_wrap(width)
  str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
  words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
  out = []
  line = []

  words.each do |word|
    if word.uncolor.length >= width
      chars = word.uncolor.split('')
      out << chars.slice!(0, width - 1).join('') while chars.count >= width
      line << chars.join('')
      next
    elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > width
      out.push(line.join(' '))
      line.clear
    end

    line << word.uncolor
  end
  out.push(line.join(' '))
  out.join("\n")
end
split_date_range() click to toggle source

Splits a range string and returns an array of DateTime objects as [start, end]. If only one date is given, end time is nil.

@return [Array<DateTime>] Start and end dates as array @example Process a natural language date range “mon 3pm to mon 5pm”.split_date_range

# File lib/doing/chronify/string.rb, line 178
def split_date_range
  time_rx = /^(\d{1,2}(:\d{1,2})?( *(am|pm))?|midnight|noon)$/
  range_rx = / (to|through|thru|(?:un)?til|-+) /

  date_string = dup

  if date_string.is_range?
    # Do we want to differentiate between "to" and "through"?
    # inclusive = date_string =~ / (through|thru|-+) / ? true : false
    inclusive = true

    dates = date_string.split(range_rx)

    if dates[0].strip =~ time_rx && dates[-1].strip =~ time_rx
      start = dates[0].strip
      finish = dates[-1].strip
    else
      start = dates[0].chronify(guess: :begin, future: false)
      finish = dates[-1].chronify(guess: inclusive ? :end : :begin, future: true)
    end

    raise Errors::InvalidTimeExpression, "Unrecognized date string (#{dates[0]})" if start.nil?

    raise Errors::InvalidTimeExpression, "Unrecognized date string (#{dates[-1]})" if finish.nil?

  else
    if date_string.strip =~ time_rx
      start = date_string.strip
      finish = '11:59pm'
    else
      start = date_string.strip.chronify(guess: :begin, future: false)
      finish = date_string.strip.chronify(guess: :end)
    end
    raise Errors::InvalidTimeExpression, 'Unrecognized date string' unless start

  end


  if start.is_a? String
    Doing.logger.debug('Parser:', "--from string interpreted as time span, from #{start || '12am'} to #{finish || '11:59pm'}")
  else
    Doing.logger.debug('Parser:', "date range interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
  end
  [start, finish]
end
tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false) click to toggle source

Add, rename, or remove a tag

@param tag The tag @param value [String] Value for tag (@tag(value)) @param remove [Boolean] Remove the tag instead of adding @param rename_to [String] Replace tag with this tag @param regex [Boolean] Tag is regular expression @param single [Boolean] Operating on a single item (for logging) @param force [Boolean] With rename_to, add tag if it doesn't exist

@return [String] The string with modified tags

# File lib/doing/string/tags.rb, line 82
def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false)
  log_level = single ? :info : :debug
  title = dup
  title.chomp!
  tag = tag.sub(/^@?/, '')
  case_sensitive = tag !~ /[A-Z]/

  rx_tag = if regex
             tag.gsub(/\./, '\S')
           else
             tag.gsub(/\?/, '.').gsub(/\*/, '\S*?')
           end

  if remove || rename_to
    rx = Regexp.new("(?<=^| )@#{rx_tag}(?<parens>\\((?<value>[^)]*)\\))?(?= |$)", case_sensitive)
    m = title.match(rx)

    if m.nil? && rename_to && force
      title.tag!(rename_to, value: value, single: single)
    elsif m
      title.gsub!(rx) do
        rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : ''
      end

      title.dedup_tags!
      title.chomp!

      if rename_to
        f = "@#{tag}".cyan
        t = "@#{rename_to}".cyan
        Doing.logger.write(log_level, 'Tag:', %(renamed #{f} to #{t} in "#{title}"))
      else
        f = "@#{tag}".cyan
        Doing.logger.write(log_level, 'Tag:', %(removed #{f} from "#{title}"))
      end
    else
      Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}")
    end
  elsif title =~ /@#{tag}(?=[ (]|$)/ && !value.good?
    Doing.logger.debug('Skipped:', "already tagged #{"@#{tag}".cyan}")
    return title
  else
    add = tag
    add += "(#{value})" unless value.nil?

    title.chomp!

    if value && title =~ /@#{tag}(?=[ (]|$)/
      title.sub!(/@#{tag}(\(.*?\))?/, "@#{add}")
    else
      title += " @#{add}"
    end

    title.dedup_tags!
    title.chomp!
    Doing.logger.write(log_level, 'Tag:', %(added #{('@' + tag).cyan} to "#{title}"))
  end

  title.gsub(/ +/, ' ')
end
tag!(tag, **options) click to toggle source

Add, rename, or remove a tag in place

@see tag

# File lib/doing/string/tags.rb, line 65
def tag!(tag, **options)
  replace tag(tag, **options)
end
time_string(format: :dhm) click to toggle source

Convert DD:HH:MM to a natural language string

@param format [Symbol] The format to output (:dhm, :hm, :m, :clock, :natural)

# File lib/doing/chronify/string.rb, line 117
def time_string(format: :dhm)
  to_seconds.time_string(format: format)
end
to_bool() click to toggle source

Returns a bool representation of the string.

@return [Boolean] Bool representation of the object.

# File lib/doing/string/query.rb, line 120
def to_bool
  case self
  when /^[yt1]/i
    true
  else
    false
  end
end
to_p(number) click to toggle source

Pluralize a string based on quantity

@param number [Integer] the quantity of the object the string represents

# File lib/doing/string/transform.rb, line 114
def to_p(number)
  number == 1 ? self : "#{self}s"
end
to_phrase_query() click to toggle source
# File lib/doing/string/query.rb, line 88
def to_phrase_query
  parser = PhraseParser::QueryParser.new
  transformer = PhraseParser::QueryTransformer.new
  parse_tree = parser.parse(self)
  transformer.apply(parse_tree).to_elasticsearch
end
to_query() click to toggle source
# File lib/doing/string/query.rb, line 95
def to_query
  parser = BooleanTermParser::QueryParser.new
  transformer = BooleanTermParser::QueryTransformer.new
  parse_tree = parser.parse(self)
  transformer.apply(parse_tree).to_elasticsearch
end
to_rx(distance: nil, case_type: nil) click to toggle source

Convert string to fuzzy regex. Characters in words can be separated by up to distance characters in haystack, spaces indicate unlimited distance.

@example “this word”.to_rx(3) # => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/

@param distance [Integer] Allowed distance between characters @param case_type The case type

@return [Regexp] Regex pattern

# File lib/doing/string/query.rb, line 63
def to_rx(distance: nil, case_type: nil)
  distance ||= Doing.config.fetch('search', 'distance', 3).to_i
  case_type ||= Doing.config.fetch('search', 'case', 'smart')&.normalize_case
  case_sensitive = case case_type
                   when :smart
                     self =~ /[A-Z]/ ? true : false
                   when :sensitive
                     true
                   else
                     false
                   end

  pattern = case dup.strip
            when %r{^/.*?/$}
              sub(%r{/(.*?)/}, '\1')
            when /^'/
              sub(/^'(.*?)'?$/, '\1')
            else
              split(/ +/).map do |w|
                w.split('').join(".{0,#{distance}}").gsub(/\+/, '\+').wildcard_to_rx
              end.join('.*?')
            end
  Regexp.new(pattern, !case_sensitive)
end
to_seconds() click to toggle source

Convert DD:HH:MM to seconds

@return [Integer] rounded number of seconds

# File lib/doing/chronify/string.rb, line 101
def to_seconds
  mtch = match(/(\d+):(\d+):(\d+)/)

  raise Errors::DoingRuntimeError, "Invalid time string: #{self}" unless mtch

  h = mtch[1]
  m = mtch[2]
  s = mtch[3]
  (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
end
to_tags() { |arr| ... } click to toggle source

Convert a list of tags to an array. Tags can be with or without @ symbols, separated by any character, and can include parenthetical values (with spaces)

@return [Array] array of tags including @ symbols

# File lib/doing/string/tags.rb, line 31
def to_tags
  arr = gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
  if block_given?
    yield arr
  else
    arr
  end
end
trunc(len, ellipsis: '...') click to toggle source

Truncate to nearest word

@param len The length

# File lib/doing/string/truncate.rb, line 13
def trunc(len, ellipsis: '...')
  return self if length <= len

  total = 0
  res = []

  split(/ /).each do |word|
    break if total + 1 + word.length > len

    total += 1 + word.length
    res.push(word)
  end
  res.join(' ') + ellipsis
end
trunc!(len, ellipsis: '...') click to toggle source
# File lib/doing/string/truncate.rb, line 28
def trunc!(len, ellipsis: '...')
  replace trunc(len, ellipsis: ellipsis)
end
truncend(len, ellipsis: '...') click to toggle source

Truncate from middle to end at nearest word

@param len The length

# File lib/doing/string/truncate.rb, line 37
def truncend(len, ellipsis: '...')
  return self if length <= len

  total = 0
  res = []

  split(/ /).reverse.each do |word|
    break if total + 1 + word.length > len

    total += 1 + word.length
    res.unshift(word)
  end
  ellipsis + res.join(' ')
end
truncend!(len, ellipsis: '...') click to toggle source
# File lib/doing/string/truncate.rb, line 52
def truncend!(len, ellipsis: '...')
  replace truncend(len, ellipsis: ellipsis)
end
truncmiddle(len, ellipsis: '...') click to toggle source

Truncate string in the middle, separating at nearest word

@param len The length @param ellipsis The ellipsis

# File lib/doing/string/truncate.rb, line 62
def truncmiddle(len, ellipsis: '...')
  return self if length <= len
  len -= (ellipsis.length / 2).to_i
  half = (len / 2).to_i
  start = trunc(half, ellipsis: ellipsis)
  finish = truncend(half, ellipsis: '')
  start + finish
end
truncmiddle!(len, ellipsis: '...') click to toggle source
# File lib/doing/string/truncate.rb, line 71
def truncmiddle!(len, ellipsis: '...')
  replace truncmiddle(len, ellipsis: ellipsis)
end
truthy?() click to toggle source

Test string for truthiness (0, “f”, “false”, “n”, “no” all return false, case insensitive, otherwise true)

@return [Boolean] String is truthy

# File lib/doing/string/query.rb, line 107
def truthy?
  if self =~ /^(0|f(alse)?|n(o)?)$/i
    false
  else
    true
  end
end
uncolor() click to toggle source

Remove color escape codes

@return clean string

# File lib/doing/string/highlight.rb, line 83
def uncolor
  gsub(/\e\[[\d;]+m/, '')
end
uncolor!() click to toggle source

@see uncolor

# File lib/doing/string/highlight.rb, line 90
def uncolor!
  replace uncolor
end
utf8() click to toggle source
# File lib/doing/string/string.rb, line 6
def utf8
  if String.method_defined? :force_encoding
    dup.force_encoding('utf-8')
  else
    self
  end
end
validate_color() click to toggle source

Extract the longest valid %color name from a string.

Allows %colors to bleed into other text and still be recognized, e.g. %greensomething still finds %green.

@return [String] a valid color name

# File lib/doing/colors.rb, line 98
def validate_color
  valid_color = nil
  compiled = ''
  normalize_color.split('').each do |char|
    compiled += char
    valid_color = compiled if Color.attributes.include?(compiled.to_sym)
  end

  valid_color
end
wildcard_to_rx() click to toggle source

Convert ? and * wildcards to regular expressions. Uses S (non-whitespace) instead of . (any character)

@return [String] Regular expression string

# File lib/doing/string/query.rb, line 44
def wildcard_to_rx
  gsub(/\?/, '\S').gsub(/\*/, '\S*?').gsub(/\]\]/, '--')
end
wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false) click to toggle source

Wrap string at word breaks, respecting tags

@param len [Integer] The length @param offset [Integer] (Optional) The width to pad each subsequent line @param prefix [String] (Optional) A prefix to add to each line

# File lib/doing/string/transform.rb, line 47
def wrap(len, pad: 0, indent: '  ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false)
  last_color = color.empty? ? '' : after.last_color
  note_rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
  note = ''
  after = after.dup if after.frozen?
  after.sub!(note_rx) do
    note = Regexp.last_match(0)
    ''
  end

  left_pad = ' ' * offset
  left_pad += indent

  # return "#{left_pad}#{prefix}#{color}#{self}#{last_color} #{note}" unless len.positive?

  # Don't break inside of tag values
  str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }.gsub(/\n/, ' ')

  words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
  out = []
  line = []

  words.each do |word|
    if word.uncolor.length >= len
      chars = word.uncolor.split('')
      out << chars.slice!(0, len - 1).join('') while chars.count >= len
      line << chars.join('')
      next
    elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > len
      out.push(line.join(' '))
      line.clear
    end

    line << word.uncolor
  end
  out.push(line.join(' '))

  last_color = ''
  out[0] = format("%-#{pad}s%s%s", out[0], last_color, after)

  out.map.with_index { |l, idx|
    if !pad_first && idx == 0
      "#{color}#{prefix}#{l}#{last_color}"
    else
      "#{left_pad}#{color}#{prefix}#{l}#{last_color}"
    end
  }.join("\n") + " #{note}".chomp
  # res.join("\n").strip + last_color + " #{note}".chomp
end