library/types/src/modules/String.rb
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File: modules/String.ycp
# Package: yast2
# Summary: String manipulation routines
# Authors: Michal Svec <msvec@suse.cz>
#
# $Id$
require "yast"
module Yast
class StringClass < Module
include Yast::Logger
# @note it is ascii chars only
UPPER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".freeze
LOWER_CHARS = "abcdefghijklmnopqrstuvwxyz".freeze
ALPHA_CHARS = UPPER_CHARS + LOWER_CHARS
DIGIT_CHARS = "0123456789".freeze
ALPHA_NUM_CHARS = ALPHA_CHARS + DIGIT_CHARS
PUNCT_CHARS = "!\"\#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".freeze
GRAPHICAL_CHARS = ALPHA_NUM_CHARS + PUNCT_CHARS
SPACE_CHARS = " \f\r\n\t\v".freeze
PRINTABLE_CHARS = SPACE_CHARS + GRAPHICAL_CHARS
def main
textdomain "base"
end
# Quote a string with 's
#
# More precisely it protects single quotes inside the string but does not
# prepend or append single quotes.
#
# @param [String] var unquoted string
# @return quoted string
# @example quote("a'b") -> "a'\''b"
def Quote(var)
return "" if var.nil?
var.gsub("'", "'\\\\''")
end
# Unquote a string with 's (quoted with quote)
# @param [String] var quoted string
# @return unquoted string
# @see #quote
def UnQuote(var)
return "" if var.nil?
log.debug "var=#{var}"
var.gsub("'\\''", "'")
end
# Optional parenthesized text
# @return " (Foo)" if Foo is neither empty or nil, else ""
def OptParens(input)
opt_format(" (%1)", input)
end
# @param [Array<String>] list of strings
# @return only non-"" items
def NonEmpty(list)
return nil unless list
list.reject { |i| i == "" }
end
# @param [String] string \n-terminated items
# @return the items as a list, with empty lines removed
def NewlineItems(string)
return nil unless string
NonEmpty(string.split("\n"))
end
# @param [Boolean] value boolean
# @return [Boolean] value as "Yes" or "No"
def YesNo(value)
# TRANSLATORS: human text for Boolean value
value ? _("Yes") : _("No")
end
# Return a pretty description of a byte count
#
# Return a pretty description of a byte count with required precision
# and using B, KiB, MiB, GiB or TiB as unit as appropriate.
#
# Uses the current locale defined decimal separator
# (i.e. the result is language dependant).
#
# @param [Fixnum] bytes size (e.g. free diskspace, memory size) in Bytes
# @param [Fixnum] precision number of fraction digits in output, if negative (less than 0) the precision is set automatically depending on the suffix
# @param [Boolean] omit_zeroes if true then do not add zeroes
# (useful for memory size - 128 MiB RAM looks better than 128.00 MiB RAM)
# @return formatted string
#
# @example FormatSizeWithPrecision(128, 2, true) -> "128 B"
# @example FormatSizeWithPrecision(4096, 2, true) -> "4 KiB"
# @example FormatSizeWithPrecision(4096, 2, false) -> "4.00 KiB"
# @example FormatSizeWithPrecision(1024*1024, 2, true) -> "1 MiB"
def FormatSizeWithPrecision(bytes, precision, omit_zeroes)
return "" if bytes.nil?
units = [
# Byte abbreviated
_("B"),
# KiloByte abbreviated
_("KiB"),
# MegaByte abbreviated
_("MiB"),
# GigaByte abbreviated
_("GiB"),
# TeraByte abbreviated
_("TiB")
]
index = 0
whole = bytes.to_f
while (whole >= 1024.0 || whole <= -1024.0) && (index + 1) < units.size
whole /= 1024.0
index += 1
end
precision ||= 0
# auto precision - depends on the suffix, but max. 3 decimal digits
precision = (index < 3) ? index : 3 if precision < 0
if omit_zeroes == true
max_difference = 0.9
max_difference /= (10.0 * precision)
precision = 0 if (whole - whole.round).abs < max_difference
end
Builtins::Float.tolstring(whole, precision) + " " + units[index]
end
# Return a pretty description of a byte count
#
# Return a pretty description of a byte count, with two fraction digits
# and using B, KiB, MiB, GiB or TiB as unit as appropriate.
#
# Uses the current locale defined decimal separator
# (i.e. the result is language dependant).
#
# @param [Fixnum] bytes size (e.g. free diskspace) in Bytes
# @return formatted string
#
# @example FormatSize(23456767890) -> "223.70 MiB"
def FormatSize(bytes)
return "" unless bytes
# automatic precision, don't print trailing zeroes for sizes < 1MiB
FormatSizeWithPrecision(bytes, -1, Ops.less_than(bytes, 1 << 20))
end
# Add a download rate status to a message.
#
# Add the current and the average download rate to the message.
#
# @param [String] text the message with %1 placeholder for the download rate string
# @param [Fixnum] avg_bps average download rate (in B/s)
# @param [Fixnum] curr_bps current download rate (in B/s)
#
# @return [String] formatted message
def FormatRateMessage(text, avg_bps, curr_bps)
rate = ""
curr_bps ||= 0
avg_bps ||= 0
if curr_bps > 0
rate = format_rate(curr_bps)
if avg_bps > 0
# format download rate message: %1 = the current download rate (e.g. "242.6kB/s")
# %2 is the average download rate (e.g. "228.3kB/s")
# to translators: keep translation of "on average" as short as possible
rate = Builtins.sformat(
_("%1 (on average %2)"),
rate,
format_rate(avg_bps)
)
end
end
# add download rate to the downloading message
# %1 is URL, %2 is formatted download rate, e.g. "242.6kB/s (avg. 228.3kB/s)"
# in ncurses UI the strings are exchanged (%1 is the rate, %2 is URL)
# due to limited space on the screen
Builtins.sformat(text, rate)
end
# Format an integer number as (at least) two digits; use leading zeroes if
# necessary.
# @param [Fixnum] input
# @return [String] number as two-digit string
#
def FormatTwoDigits(input)
msg = (0..9).member?(input) ? "0%1" : "%1"
Builtins.sformat(msg, input)
end
# Format an integer seconds value with min:sec or hours:min:sec
# @param [Fixnum] seconds time (in seconds)
# @return [String] formatted string (empty for negative values)
#
def FormatTime(seconds)
return "nil:nil:nil" unless seconds # funny backward compatibility
return "" if seconds < 0
if seconds < 3600 # Less than one hour
Builtins.sformat(
"%1:%2",
FormatTwoDigits(seconds / 60),
FormatTwoDigits(seconds % 60)
) # More than one hour - we don't hope this will ever happen, but who knows?
else
hours = seconds / 3600
seconds = seconds % 3600
Builtins.sformat(
"%1:%2:%3",
hours,
FormatTwoDigits(seconds / 60),
FormatTwoDigits(seconds % 60)
)
end
end
# Remove spaces and tabs at begin and end of input string.
# @param [String] input string to be stripped
# @return stripped string
# @deprecated if remove also \n then use {::String#strip}, otherwise simple sub is enough
# @example CutBlanks(" \tany input ") -> "any input"
def CutBlanks(input)
return "" if input.nil?
input.sub(/\A[ \t]*(.*[^ \t])[ \t]*\z/, "\\1")
end
# Remove any leading zeros
#
# Remove any leading zeros that make tointeger inadvertently
# assume an octal number (e.g. "09" -> "9", "0001" -> "1",
# but "0" -> "0")
#
# @param [String] input number that might contain leadig zero
# @return [String] that has leading zeros removed
# @deprecated if conversion to integer is needed in decimal use {::String#to_i}
def CutZeros(input)
return "" if input.nil?
input.sub(/\A0*([0-9])/, "\\1")
end
# Repeat a string
#
# Repeat a string number of times.
#
# @param input string to repeat
# @param input number number of repetitions
# @return [String] repeated string
# @deprecated use {::String#operator*} instead
def Repeat(text, number)
return "" if text.nil? || number.nil? || number < 1
text * number
end
# Add the padding character around the text to make it long enough
#
# Add the padding character around the text to make it long enough. If the
# text is longer than requested, no changes are made.
#
# @param [String] text text to be padded
# @param [Fixnum] length requested length
# @param [String] padding padding character
# @param [Symbol] alignment alignment to use, either `left or `right
# @return padded text
def SuperPad(text, length, padding, alignment)
text ||= ""
return text if length.nil? || text.size >= length || padding.nil?
pad = padding * (length - text.size)
(alignment == :right) ? (pad + text) : (text + pad)
end
# Add spaces after the text to make it long enough
#
# Add spaces after the text to make it long enough. If the text is longer
# than requested, no changes are made.
#
# @param [String] text text to be padded
# @param [Fixnum] length requested length
# @return padded text
def Pad(text, length)
SuperPad(text, length, " ", :left)
end
# Add zeros before the text to make it long enough.
#
# Add zeros before the text to make it long enough. If the text is longer
# than requested, no changes are made.
#
# @param [String] text text to be padded
# @param [Fixnum] length requested length
# @return padded text
def PadZeros(text, length)
SuperPad(text, length, "0", :right)
end
# Parse string of values
#
# Parse string of values - split string to values, quoting and backslash sequences are supported
# @param [String] options Input string
# @param [Hash] parameters Parmeter used at parsing - map with keys:
# "separator":<string> - value separator (default: " \t"),
# "unique":<boolean> - result will not contain any duplicates, first occurance of the string is stored into output (default: false),
# "interpret_backslash":<boolean> - convert backslash sequence into one character (e.g. "\\n" => "\n") (default: true)
# "remove_whitespace":<boolean> - remove white spaces around values (default: true),
# @return [Array<String>] List of strings
def ParseOptions(options, parameters)
parameters ||= {}
ret = []
# parsing options
separator = parameters["separator"] || " \t"
unique = parameters.fetch("unique", false)
interpret_backslash = parameters.fetch("interpret_backslash", true)
remove_whitespace = parameters.fetch("remove_whitespace", true)
log.debug "Input: string: '#{options}', parameters: #{parameters}"
return [] unless options
# two algorithms are used:
# first is much faster, but only usable if string
# doesn't contain any double qoute characters
# and backslash sequences are not interpreted
# second is more general, but of course slower
if options.include?("\"") && !interpret_backslash
# easy case - no qouting, don't interpres backslash sequences => use splitstring
values = options.split(/[#{separator}]/)
values.each do |v|
v = CutBlanks(v) if remove_whitespace == true
ret << v if !unique || !ret.include?(v)
end
else
# quoting is used or backslash interpretation is enabled
# so it' not possible to split input
# parsing each character is needed - use finite automaton
# state
state = :out_of_string
# position in the input string
index = 0
# parsed value - buffer
str = ""
while index < options.size
character = options[index]
log.debug "character: #{character} state: #{state} index: #{index}"
# interpret backslash sequence
if character == "\\" && interpret_backslash
nextcharacter = options[index + 1]
if nextcharacter
index += 1
# backslah sequences
backslash_seq = {
"a" => "\a", # alert
"b" => "\b", # backspace
"e" => "\e", # escape
"f" => "\f", # FF
"n" => "\n", # NL
"r" => "\r", # CR
"t" => "\t", # tab
"v" => "\v", # vertical tab
"\\" => "\\", # backslash
# backslash will be removed later,
# double quote and escaped double quote have to be different
# as it have different meaning
"\"" => "\\\""
}
character = backslash_seq[nextcharacter] || nextcharacter
log.debug "backslash sequence: '#{character}'"
else
log.warn "Missing character after backslash (\\) at the end of string"
end
end
case state
when :out_of_string
# ignore separator or white space at the beginning of the string
if separator.include?(character) || (remove_whitespace && character =~ /[ \t]/)
index += 1
next
# start of a quoted string
elsif character == "\""
state = :in_quoted_string
else
# start of a string
state = :in_string
str = (character == "\\\"") ? "\"" : character
end
# after double quoted string - handle non-separator chars after double quote
when :in_quoted_string_after_dblqt
if separator.include?(character)
ret << str if !unique || !Builtins.contains(ret, str)
str = ""
state = :out_of_string
elsif character == "\\\""
str << "\""
else
str << character
end
when :in_quoted_string
case character
when "\""
# end of quoted string
state = :in_quoted_string_after_dblqt
when "\\\""
str << "\""
else
str << character
end
when :in_string
if separator.include?(character)
state = :out_of_string
str = CutBlanks(str) if remove_whitespace
ret << str if !unique || !ret.include?(str)
str = ""
elsif character == "\\\""
str << "\""
else
str << character
end
end
index += 1
end
# error - still in quoted string
if [:in_quoted_string, :in_quoted_string_after_dblqt].include?(state)
log.warn "Missing trainling double quote character(\") in input: '#{options}'" if state == :in_quoted_string
ret << str if !unique || !ret.include?(str)
end
# process last string in the buffer
if state == :in_string
str = CutBlanks(str) if remove_whitespace
ret << str if !unique || !ret.include?(str)
end
end
log.debug "Parsed values: #{ret}"
ret
end
# Remove first or every match of given regular expression from a string
#
# (e.g. CutRegexMatch( "abcdef12ef34gh000", "[0-9]+", true ) -> "abcdefefgh",
# CutRegexMatch( "abcdef12ef34gh000", "[0-9]+", false ) -> "abcdefef34gh000")
#
# @param [String] input string that might occur regex
# @param [String] regex regular expression to search for, must not contain brackets
# @param [Boolean] glob flag if only first or every occuring match should be removed
# @return [String] that has matches removed
def CutRegexMatch(input, regex, glob)
return "" if input.nil? || input.empty?
output = input
if Builtins.regexpmatch(output, regex)
p = Builtins.regexppos(output, regex)
loop do
first_index = p[0]
lenght = p[1] || 0
output = output[0, first_index] + output[(first_index + lenght)..-1]
p = Builtins.regexppos(output, regex)
break unless glob
break if p.empty?
end
end
output
end
# Function for escaping (replacing) (HTML|XML...) tags with their
# (HTML|XML...) meaning.
#
# Usable to present text "as is" in RichText.
#
# @param [String] text to escape
# @return [String] escaped text
def EscapeTags(text)
return nil unless text
text = text.dup
text.gsub!("&", "&")
text.gsub!("<", "<")
text.gsub!(">", ">")
text
end
# Shorthand for select (splitstring (s, separators), 0, "")
# Useful now that the above produces a deprecation warning.
# @param [String] string to be split
# @param [String] separators characters which delimit components
# @return first component or ""
def FirstChunk(string, separators)
return "" if !string || !separators
string[/\A[^#{separators}]*/]
end
# The 26 lowercase ASCII letters
def CLower
LOWER_CHARS
end
# The 52 upper and lowercase ASCII letters
def CAlpha
ALPHA_CHARS
end
# Digits: 0123456789
def CDigit
DIGIT_CHARS
end
# The 62 upper and lowercase ASCII letters and digits
def CAlnum
ALPHA_NUM_CHARS
end
# Printable ASCII charcters except whitespace, 33-126
def CGraph
GRAPHICAL_CHARS
end
# Printable ASCII characters including whitespace
def CPrint
PRINTABLE_CHARS
end
# Characters valid in a filename (not pathname).
# Naturally "/" is disallowed. Otherwise, the graphical ASCII
# characters are allowed.
# @return [String] for ValidChars
def ValidCharsFilename
GRAPHICAL_CHARS.delete("/")
end
# Function creates text table without using HTML tags.
# (Useful for commandline)
# Undefined option uses the default one.
#
# @param [Array<String>] header
# @param [Array<Array<String>>] items
# @param [Hash{String => Object}] options
# @return [String] table
#
# Header: [ "Id", "Configuration", "Device" ]
# Items: [ [ "1", "aaa", "Samsung Calex" ], [ "2", "bbb", "Trivial Trinitron" ] ]
# Possible Options: horizontal_padding (for columns), table_left_padding (for table)
def TextTable(header, items, options)
options ||= {}
items ||= []
current_horizontal_padding = options["horizontal_padding"] || 2
current_table_left_padding = options["table_left_padding"] || 4
cols_lenghts = find_longest_records(Builtins.add(items, header))
# whole table is left-padded
table_left_padding = Pad("", current_table_left_padding)
# the last row has no newline
rows_count = items.size
table = ""
table << table_left_padding
table << table_row(header, cols_lenghts, current_horizontal_padding)
table << "\n"
table << table_left_padding
table << table_header_underline(cols_lenghts, current_horizontal_padding)
table << "\n"
items.each_with_index do |row, rows_counter|
table << table_left_padding
table << table_row(row, cols_lenghts, current_horizontal_padding)
table << "\n" if (rows_counter + 1) < rows_count
end
table
end
# Function returns underlined text header without using HTML tags.
# (Useful for commandline)
#
# @param string header line
# @param integer left padding
# @return [String] underlined header line
def UnderlinedHeader(header_line, left_padding)
return nil unless header_line
left_padding ||= 0
Pad("", left_padding) + header_line + "\n" +
Pad("", left_padding) + underline(header_line.size)
end
# Replace substring in a string. All substrings source are replaced by string target.
# @param [String] input string
# @param [String] source the string which will be replaced
# @param [String] target the new string which is used instead of source
# @return [String] result
def Replace(input, source, target)
return nil if input.nil?
if source.nil? || source == ""
Builtins.y2warning("invalid parameter source: %1", source)
return input
end
if target.nil?
Builtins.y2warning("invalid parameter target: %1", target)
return input
end
# avoid infinite loop even if it break backward compatibility
raise "Target #{target} include #{source} which will lead to infinite loop" if target.include?(source)
pos = input.index(source)
while pos
tmp = input[0, pos] + target
tmp << input[(pos + source.size)..-1] if input.size > (pos + source.size)
input = tmp
pos = input.index(source)
end
input
end
# Make a random base-36 number.
# srandom should be called beforehand.
# @param [Fixnum] len string length
# @return random string of 0-9 and a-z
def Random(len)
return "" if !len || len <= 0
digits = DIGIT_CHARS + LOWER_CHARS # uses the character classes from above
ret = Array.new(len) { digits[rand(digits.size)] }
ret.join
end
# Format file name - truncate the middle part of the directory to fit to the reqested lenght.
# Path elements in the middle of the string are replaced by ellipsis (...).
# The result migth be longer that requested size if size of the last element
# (with ellipsis) is longer than the requested size. If the requested size is greater than
# size of the input then the string is not modified. The last part (file name) is never removed.
#
# @example FormatFilename("/really/very/long/file/name", 15) -> "/.../file/name"
# @example FormatFilename("/really/very/long/file/name", 5) -> ".../name"
# @example FormatFilename("/really/very/long/file/name", 100) -> "/really/very/long/file/name"
#
# @param [String] file_path file name
# @param [Fixnum] len requested maximum lenght of the output
# @return [String] Truncated file name
def FormatFilename(file_path, len)
return nil unless file_path
return file_path if len && len > file_path.size
dir = file_path.split("/", -1)
file = dir.pop
# there is a slash at the end, add the directory name
file = dir.pop + "/" if file == ""
if dir.join("/").size <= 3
# the path is short, replacing by ... cannot help
return file_path
end
ret = ""
loop do
# ellipsis - used to replace part of text to make it shorter
# example: "/really/very/long/file/name", "/.../file/name")
ellipsis = _("...")
dir[dir.size / 2] = ellipsis
ret = (dir + [file]).join("/")
break unless len # funny backward compatibility that for nil len remove one element
# the size is OK
break if ret.size <= len
# still too long, remove the ellipsis and start a new iteration
dir.delete(ellipsis)
break if dir.empty?
end
ret
end
# Remove a shortcut from a label, so that it can be inserted into help
# to avoid risk of different translation of the label
# @param [String] label string a label possibly including a shortcut
# @return [String] the label without the shortcut mark
def RemoveShortcut(label)
ret = label
if Builtins.regexpmatch(label, "^(.*[^&])?(&&)*&[[:alnum:]].*$")
ret = Builtins.regexpsub(
label,
"^((.*[^&])?(&&)*)&([[:alnum:]].*)$",
"\\1\\4"
)
end
ret
end
# Checks whether string str starts with test.
def StartsWith(str, test)
Builtins.search(str, test) == 0
end
# Find a mount point for given directory/file path. Returns "/" if no mount point matches
# @param dir requested path , e.g. "/usr/share"
# @param dirs list of mount points, e.g. [ "/", "/usr", "/boot" ]
# @return string a mount point from the input list or "/" if not found
def FindMountPoint(dir, dirs)
dirs = deep_copy(dirs)
while !dir.nil? && dir != "" && !Builtins.contains(dirs, dir)
# strip the last path component and try it again
comps = Builtins.splitstring(dir, "/")
comps = Builtins.remove(comps, Ops.subtract(Builtins.size(comps), 1))
dir = Builtins.mergestring(comps, "/")
end
dir = "/" if dir.nil? || dir == ""
dir
end
publish function: :Quote, type: "string (string)"
publish function: :UnQuote, type: "string (string)"
publish function: :OptParens, type: "string (string)"
publish function: :NonEmpty, type: "list <string> (list <string>)"
publish function: :NewlineItems, type: "list <string> (string)"
publish function: :YesNo, type: "string (boolean)"
publish function: :FormatSizeWithPrecision, type: "string (integer, integer, boolean)"
publish function: :FormatSize, type: "string (integer)"
publish function: :FormatRateMessage, type: "string (string, integer, integer)"
publish function: :FormatTime, type: "string (integer)"
publish function: :CutBlanks, type: "string (string)"
publish function: :CutZeros, type: "string (string)"
publish function: :Repeat, type: "string (string, integer)"
publish function: :SuperPad, type: "string (string, integer, string, symbol)"
publish function: :Pad, type: "string (string, integer)"
publish function: :PadZeros, type: "string (string, integer)"
publish function: :ParseOptions, type: "list <string> (string, map)"
publish function: :CutRegexMatch, type: "string (string, string, boolean)"
publish function: :EscapeTags, type: "string (string)"
publish function: :FirstChunk, type: "string (string, string)"
publish function: :CLower, type: "string ()"
publish function: :CAlpha, type: "string ()"
publish function: :CDigit, type: "string ()"
publish function: :CAlnum, type: "string ()"
publish function: :CGraph, type: "string ()"
publish function: :CPrint, type: "string ()"
publish function: :ValidCharsFilename, type: "string ()"
publish function: :TextTable, type: "string (list <string>, list <list <string>>, map <string, any>)"
publish function: :UnderlinedHeader, type: "string (string, integer)"
publish function: :Replace, type: "string (string, string, string)"
publish function: :Random, type: "string (integer)"
publish function: :FormatFilename, type: "string (string, integer)"
publish function: :RemoveShortcut, type: "string (string)"
publish function: :StartsWith, type: "boolean (string, string)"
publish function: :FindMountPoint, type: "string (string, list <string>)"
private
# Optional formatted text
# @return sformat (f, s) if s is neither empty or nil, else ""
def opt_format(format, string)
(string == "" || string.nil?) ? "" : Builtins.sformat(format, string)
end
# Return a pretty description of a download rate
#
# Return a pretty description of a download rate, with two fraction digits
# and using B/s, KiB/s, MiB/s, GiB/s or TiB/s as unit as appropriate.
#
# @param [Fixnum] bytes_per_second download rate (in B/s)
# @return formatted string
#
# @example format_rate(6780) -> ""
# @example format_rate(0) -> ""
# @example format_rate(895321) -> ""
def format_rate(bytes_per_second)
# covert a number to download rate string
# %1 is string - size in bytes, B, KiB, MiB, GiB or TiB
Builtins.sformat(_("%1/s"), FormatSize(bytes_per_second))
end
# Local function returns underline string /length/ long.
#
# @param integer length of underline
# @return string /length/ long underline
def underline(length)
return "" unless length
"-" * length
end
# Local function for creating header underline for table.
# It uses maximal lengths of records defined in cols_lenghts.
#
# @param list <integer> maximal lengths of records in columns
# @param integer horizontal padding of records
# @return string table header underline
def table_header_underline(cols_lenghts, horizontal_padding)
horizontal_padding ||= 0
# count of added paddings
records_count = cols_lenghts.size - 1
# total length of underline
total_size = 0
cols_lenghts.each_with_index do |col_size, col_counter|
total_size += col_size
# adding padding where necessary
total_size += horizontal_padding if col_counter < records_count
end
underline(total_size)
end
# Local function for finding longest records in the table.
#
# @param list <list <string> > table items
# @return list <integer> longest records by columns
def find_longest_records(items)
return [] unless items
# searching all rows
items.each_with_object([]) do |row, result|
next unless row
row.each_with_index do |e, i|
size = e ? e.size : 0
result[i] ||= size
result[i] = size if size > result[i]
end
end
end
# Local function creates table row.
#
# @param list <string> row items
# @param list <integer> columns lengths
# @param integer record horizontal padding
# @return string padded table row
def table_row(row_items, cols_lenghts, horizontal_padding)
row = ""
row_items ||= []
records_count = row_items.size - 1
row_items.each_with_index do |record, col_counter|
padding = cols_lenghts[col_counter] || 0
padding += horizontal_padding if col_counter < records_count
row << Pad(record, padding)
end
row
end
end
String = StringClass.new
String.main
end