You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
calculate-utils-3-lib/pym/calculate/lib/utils/text.py

431 lines
14 KiB

# -*- coding: utf-8 -*-
# Copyright 2008-2016 Mir Calculate. http://www.calculate-linux.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .. import cl_overriding
import re
import sys
from itertools import *
from ..cl_lang import setLocalTranslate
_ = lambda x: x
setLocalTranslate('cl_lib3', sys.modules[__name__])
# _u = lambda t: t.decode('UTF-8', 'replace') if isinstance(t, str) else t
_u = lambda t: t if isinstance(t, str) else t
_uu = lambda *tt: tuple(_u(t) for t in tt)
# _u8 = lambda t: t.encode('UTF-8', 'replace') if isinstance(t, str) else t
_u8 = lambda t: t
_uu8 = lambda *tt: tuple(_u8(t) for t in tt)
def columnMatrix(*cols):
"""
Split columns text to matrix
"""
def splitter(s, wid):
"""Split string by width and \\n"""
for part in _u(s).split('\n'):
if wid > 0:
while part:
p, part = part[:wid], part[wid:]
yield p
else:
yield part
return list(zip_longest(
*starmap(splitter,
zip_longest(islice(cols, 0, None, 2),
islice(cols, 1, None, 2))),
fillvalue=""))
def columnStr(*cols):
"""
Get string for write, data by columns
Parameters:
cols list(tuple(text,width))
Return:
string for display
Example: columnStr( "Some text", 10, "Next column", 20 )
"""
def generateStrings(*cols):
colWidth = list(islice(cols, 1, None, 2))
for line in (zip_longest(colWidth, x) for x in columnMatrix(*cols)):
yield " ".join(("{0:%d}" % (width or 1)).format(s) for \
width, s in line)
return "\n".join(generateStrings(*cols))
def justify(s, width):
"""
Выровнить текст по ширине
Параметры:
s выводимая строка
width ширина на которую надо выровнить строку
Возвращаямые параметры:
Выровненная строка
"""
# если подана строка без пробелов - прекратить обработку
if s.find(' ') == -1:
return s
pos = 0
# переводим в юникод для правильного вычисления длины
try:
s = s.decode('utf-8')
# пропуск если это не utf-8
except UnicodeEncodeError:
pass
# пока длина строки меньше указанной
while len(s) < width:
# находим очередной пробел
pos = s.find(' ', pos)
# если не найден искать сначала
if pos == -1:
pos = s.find(' ')
# вставить в позицию еще один пробел
s = s[:pos] + ' ' + s[pos:]
# оставить удвоенный пробел
pos += 3
# вернуть строку в utf8 если она пришла в utf8
return s.encode('utf-8')
class tableReport():
"""Display data in table"""
def __init__(self, title, headerList, dataList, colSpan=1):
# title
self.title = title
# elements list for first table row
self.headerList = headerList
# row list (each row is list)
self.dataList = [[str(y).expandtabs() for y
in [z or "" for z in x]] for x in dataList]
# calculate columns list
self.columnsWidth = self.getColumsnWidth()
self.vertChar = colSpan * " " + "|" + colSpan * " "
self.crossChar = colSpan * "-" + "+" + colSpan * "-"
self.colSpan = colSpan
self.align = "<" + "^" * (len(headerList) - 2) + "<"
def setAutosize(self, colNum=-1, maxSize=None, forceMax=False):
"""
Set width for specified column
"""
if maxSize is None:
maxSize = get_term_size()[1]
if not maxSize:
maxSize = 80
spanSize = self.colSpan * 2 + 1
if colNum < 0:
colNum += len(self.columnsWidth)
currentSize = sum((x + spanSize for x in self.columnsWidth)) + 1
if currentSize < maxSize and not forceMax:
return
excludeSize = sum((x[1] + spanSize for x
in enumerate(self.columnsWidth) if x[0] != colNum)) + spanSize + 1
if maxSize - excludeSize > 5:
self.columnsWidth[colNum] = maxSize - excludeSize
def getColumsnWidth(self):
"""Находит максимальную ширину каждой колонки
результат список ширин колонок
"""
columnsWidth = []
for s in self.headerList:
columnsWidth.append(len(_u(s)))
lenCol = len(self.headerList)
maxLenCol = lenCol
# Вычисляем максимальное количество столбцов
for s in self.dataList:
lenS = len(s)
if maxLenCol < lenS:
maxLenCol = lenS
if maxLenCol > lenCol:
appCol = maxLenCol - lenCol
# Добавляем элементы в список ширин
for i in range(appCol):
columnsWidth.append(0)
# Вычисляем ширину столбцов
for e in self.dataList:
for i, s in enumerate(e):
lenS = len(_u(s))+1
if columnsWidth[i] < lenS:
columnsWidth[i] = lenS
return columnsWidth
def createFormatStr(self, listStr, offset=0):
"""Создает список (текст, ширина ...)"""
return chain(*zip_longest(listStr,
[x-offset for x in self.columnsWidth], fillvalue=""))
def printFunc(self, s):
"""
Overriding print
"""
cl_overriding.printSUCCESS(s, printBR=False)
def line_printer(self, s):
"""
Вывод используется для отображения "линий" таблицы
"""
self.printFunc(s)
def head_printer(self, s):
"""
Вывод используется для отображения текста в шапке
"""
self.printFunc(s)
def body_printer(self, s):
"""
Вывод используется для отображения текста в таблице
"""
self.printFunc(s)
def default_printer(self, s):
self.printFunc(s)
def printReport(self, printRows=True):
"""Напечатать данные в табличном виде"""
if self.title:
self.printFunc(self.title)
listStrSep = []
for lenCol in self.columnsWidth:
listStrSep.append("-" * lenCol)
printData = [[self.crossChar, self.line_printer,
columnMatrix(*self.createFormatStr(listStrSep))],
[self.vertChar, self.head_printer,
columnMatrix(*self.createFormatStr(self.headerList))],
[self.crossChar, self.line_printer,
columnMatrix(*self.createFormatStr(listStrSep))]]
for s in self.dataList:
printData.append([self.vertChar, self.body_printer,
columnMatrix(*self.createFormatStr(s, 1))])
printData.append([self.crossChar, self.line_printer,
columnMatrix(*self.createFormatStr(listStrSep))])
last_col = len(self.headerList) - 1
for char, _print, row in printData:
for line in row:
self.line_printer(char[self.colSpan:])
for i, width, txt in zip(count(),
self.columnsWidth, line):
if _print == self.body_printer:
align = "<"
offset = " "
if width:
width -= 1
else:
align = "^"
offset = ""
_print(("%s{0:%s%d}" % (offset, align, width)).format(txt))
if i < last_col:
self.line_printer(char)
self.line_printer(char[:-self.colSpan + 1])
self.default_printer('\n')
if printRows:
self.printFunc("(%s %s)" % (len(self.dataList), _("rows")))
class MultiReplace:
"""MultiReplace function object
Usage:
replacer = MultiReplace({'str':'efg','in':'asd'})
s = replacer("string")
"""
def __init__(self, repl_dict):
# string to string mapping; use a regular expression
keys = list(repl_dict.keys())
keys.sort(reverse=True) # lexical order
pattern = "|".join([re.escape(key) for key in keys])
self.pattern = re.compile(pattern)
self.dict = repl_dict
def replace(self, s):
# apply replacement dictionary to string
def repl(match, get=self.dict.get):
item = match.group(0)
return get(item, item)
return self.pattern.sub(repl, s)
__call__ = replace
def str2dict(s):
"""Convert string to dictionary:
String format:
{'key1':'value1','key2':'val\'ue2'}
"""
value = r'(?:\\\\|\\\'|[^\'])'
pair = r"""
\s*' # begin key
(%(v)s*)
' # end key
\s*:\s* # delimeter key/value
' # begin value
(%(v)s*)
'\s* # end value
""" % {"v": value}
reDict = re.compile(pair, re.X)
reMatchDict = re.compile("""
^{ # begin dict
((%(v)s,)* # many pair with comma at end
%(v)s)? # pair without comma
}$ # end dict
""" % {'v': pair}, re.X)
if reMatchDict.match(s.strip()):
d = dict(reDict.findall(s))
replaceSlash = MultiReplace({'\\\\': '\\', '\\\'': '\''})
for i in d.keys():
d[i] = replaceSlash(d[i])
return d
else:
cl_overriding.printERROR(_("wrong dict value: %s" % s))
cl_overriding.exit(1)
def str2list(s):
"""Convert string to list:
String format:
['value1','val\'ue2']
"""
value = r'(?:\\\\|\\\'|[^\'])'
element = r"""
\s*' # begin value
(%(v)s*)
'\s* # end value
""" % {"v": value}
reList = re.compile(element, re.X)
reMatchList = re.compile("""
^\[ # begin dict
((%(v)s,)* # many elements with comma at end
%(v)s)? # element without comma
\]$ # end dict
""" % {'v': element}, re.X)
if reMatchList.match(s.strip()):
replaceSlash = MultiReplace({'\\\\': '\\', '\\\'': '\''})
return [replaceSlash(i) for i in reList.findall(s)]
else:
cl_overriding.printERROR(_("wrong list value: %s" % s))
cl_overriding.exit(1)
def list2str(list):
"""Convert list to string
Return string with escaped \ and '.
"""
replaceSlash = MultiReplace({'\\': '\\\\', '\'': '\\\''})
return "[%s]" % ','.join(["'%s'" % replaceSlash(str(i))
for i in list])
def dict2str(dict):
"""Convert dictionry to string
Return string with escaped \ and '.
"""
replaceSlash = MultiReplace({'\\': '\\\\', '\'': '\\\''})
return '{%s}' % ','.join(["'%s':'%s'" % (str(k), replaceSlash(str(v))) \
for (k, v) in dict.items()])
def convertStrListDict(val):
"""Convert data between string, list, dict"""
# if val is list
if isinstance(val, list):
return list2str(val)
# if val is dictionary
elif isinstance(val, dict):
return dict2str(val)
# else it is string
else:
# detect dictionary
if re.match("^{.*}$", val):
return str2dict(val)
# detect list
elif re.match("^\[.*\]$", val):
return str2list(val)
# else is simple string
else:
return val
def formatListOr(lst):
"""Convert list to string like this: [1,2,3] -> 1,2 or 3"""
lststr = [str(x) for x in lst]
return (" %s " % _("or")).join((x for x in [", ".join((y or "''" for y in lststr[:-1])),"".join(lststr[-1:])] if x))
def get_term_size(fd=sys.stdout.fileno()):
"""
Get terminal size
"""
try:
import struct
import fcntl
import termios
except ImportError:
struct, fcntl, termios = None, None, None
return None, None
try:
s = struct.pack("HHHH", 0, 0, 0, 0)
data = fcntl.ioctl(fd, termios.TIOCGWINSZ, s)
height, width = struct.unpack('4H', data)[:2]
return height, width
except Exception:
return None, None
def simplify_profiles(profiles, drop_words=("amd64", "x86")):
"""
Упростить названия профилей (удалить одинаковое начало и конец в строках)
:param profiles: список названий профилей
:param drop_words: список слов из профилей, которые можно отбросить
если профиль один
:return: список упрощенных названий профилей
"""
if len(profiles) < 2:
if len(profiles) == 1:
return ["/".join((x for x in profiles[0].split('/') if x not in drop_words))]
return profiles
matrix = [x.split('/') for x in profiles]
# удаляем одинаковые начальные и конечные поля
matrix = zip(*reversed(list(dropwhile(lambda x: x.count(x[0]) == len(x),
(reversed(list(dropwhile(
lambda x: x.count(x[0]) == len(x),
zip_longest(*matrix,
fillvalue="")))))))))
return ["/".join((y for y in x if y)) for x in matrix]