Skip to content

Commit

Permalink
- Added 3 formatters: fmt_prefixed_unit, fmt_time_delta, `fmt_aut…
Browse files Browse the repository at this point in the history
…o_float`.
  • Loading branch information
delameter committed May 15, 2022
1 parent 3e28b02 commit e026643
Show file tree
Hide file tree
Showing 14 changed files with 583 additions and 48 deletions.
2 changes: 1 addition & 1 deletion .env.dist
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION=1.7.3
VERSION=1.7.4
PYPI_USERNAME=__token__
PYPI_PASSWORD= #api token
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,10 @@ You can of course create your own sequences and formats, but with one limitation

## Changelog

### v1.7.4

- Added 3 formatters: `fmt_prefixed_unit`, `fmt_time_delta`, `fmt_auto_float`.

### v1.7.3

- Added `bg_black` format.
Expand Down
4 changes: 4 additions & 0 deletions dev/readme/README.tpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,10 @@ You can of course create your own sequences and formats, but with one limitation

## Changelog

### v1.7.4

- Added 3 formatters: `fmt_prefixed_unit`, `fmt_time_delta`, `fmt_auto_float`.

### v1.7.3

- Added `bg_black` format.
Expand Down
15 changes: 11 additions & 4 deletions pytermor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@
'Format',

'apply_filters',
'ljust_fmtd',
'rjust_fmtd',
'center_fmtd',
'StringFilter',
'ReplaceCSI',
'ReplaceSGR',
'ReplaceNonAsciiBytes',

'ljust_fmtd',
'rjust_fmtd',
'center_fmtd',

'fmt_prefixed_unit',
'fmt_time_delta',
'fmt_auto_float',
'PrefixedUnitFmtPreset',
'TimeDeltaFmtPreset',
]
__version__ = '1.7.3'
__version__ = '1.7.4'
28 changes: 28 additions & 0 deletions pytermor/util/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -----------------------------------------------------------------------------
# pytermor [ANSI formatted terminal output toolset]
# (C) 2022 A. Shavykin <0.delameter@gmail.com>
# -----------------------------------------------------------------------------
from .filter import *
from .fmtd import *

from .auto_float import *
from .prefixed_unit import *
from .time_delta import *

__all__ = [
'apply_filters',
'StringFilter',
'ReplaceCSI',
'ReplaceSGR',
'ReplaceNonAsciiBytes',

'ljust_fmtd',
'rjust_fmtd',
'center_fmtd',

'fmt_prefixed_unit',
'fmt_time_delta',
'fmt_auto_float',
'PrefixedUnitFmtPreset',
'TimeDeltaFmtPreset',
]
38 changes: 38 additions & 0 deletions pytermor/util/auto_float.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -----------------------------------------------------------------------------
# pytermor [ANSI formatted terminal output toolset]
# (C) 2022 A. Shavykin <0.delameter@gmail.com>
# -----------------------------------------------------------------------------
from math import trunc


def fmt_auto_float(value: float, max_len: int, output_int: bool) -> str:
"""
Dynamically adjust decimal digits amount to fill up output string
with significant digits as much as possible.
Examples:
- auto_float(1234.56, 4, False) -> 1235
- auto_float(123.56, 4, False) -> 124
- auto_float(12.56, 4, False) -> 12.6
- auto_float(1.56, 4, False) -> 1.56
- auto_float(1234.56, 4, True) -> 1235
- auto_float(12.56, 4, True) -> 13
:param value: value to format
:param max_len: maximum output string length (total)
:param output_int: omit decimal point and everything to the right of it
:return: formatted value
"""
if not output_int:
max_decimals_len = 2
integer_len = len(str(trunc(value)))
decimals_and_point_len = min(max_decimals_len + 1, max_len - integer_len)

decimals_len = 0
if decimals_and_point_len >= 2: # dot without decimals makes no sense
decimals_len = decimals_and_point_len - 1
dot_str = f'.{decimals_len!s}'
else:
dot_str = '.0'

return f'{value:{max_len}{dot_str}f}'
11 changes: 11 additions & 0 deletions pytermor/util/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# -----------------------------------------------------------------------------
# pytermor [ANSI formatted terminal output toolset]
# (C) 2022 A. Shavykin <0.delameter@gmail.com>
# -----------------------------------------------------------------------------

def get_terminal_width():
try:
import shutil as _shutil
return _shutil.get_terminal_size().columns - 2
except ImportError:
return 80
22 changes: 1 addition & 21 deletions pytermor/util.py → pytermor/util/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import re
from functools import reduce
from typing import Callable, AnyStr, Type, Generic
from typing import Generic, AnyStr, Callable, Type


class StringFilter(Generic[AnyStr]):
Expand Down Expand Up @@ -40,23 +40,3 @@ def __init__(self, repl: bytes = b'?'):
def apply_filters(string: AnyStr, *args: StringFilter|Type[StringFilter]) -> AnyStr:
filters = map(lambda t: t() if isinstance(t, type) else t, args)
return reduce(lambda s, f: f.apply(s), filters, string)


def ljust_fmtd(s: str, width: int, fillchar: str = ' ') -> str:
sanitized = ReplaceSGR().apply(s)
return s + fillchar * max(0, width - len(sanitized))


def rjust_fmtd(s: str, width: int, fillchar: str = ' ') -> str:
sanitized = ReplaceSGR().apply(s)
return fillchar * max(0, width - len(sanitized)) + s


def center_fmtd(s: str, width: int, fillchar: str = ' ') -> str:
sanitized = ReplaceSGR().apply(s)
fill_len = max(0, width - len(sanitized))
if fill_len == 0:
return s
right_fill_len = fill_len // 2
left_fill_len = fill_len - right_fill_len
return (fillchar * left_fill_len) + s + (fillchar * right_fill_len)
45 changes: 45 additions & 0 deletions pytermor/util/fmtd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -----------------------------------------------------------------------------
# pytermor [ANSI formatted terminal output toolset]
# (C) 2022 A. Shavykin <0.delameter@gmail.com>
# -----------------------------------------------------------------------------
from . import ReplaceSGR


def ljust_fmtd(s: str, width: int, fillchar: str = ' ') -> str:
"""
Correctly justifies input that can include SGR sequences. Apart from
that is very similar to regular str.ljust().
:param s: string to extend
:param width: target string length
:param fillchar: append this char to target
:return: **s** padded to the left side with *fillchars* so that now it's
length corresponds to *width*
"""
sanitized = ReplaceSGR().apply(s)
return s + fillchar * max(0, width - len(sanitized))


def rjust_fmtd(s: str, width: int, fillchar: str = ' ') -> str:
"""
SGR-aware implementation of str.rjust(). @see: ljust_fmtd
"""
sanitized = ReplaceSGR().apply(s)
return fillchar * max(0, width - len(sanitized)) + s


def center_fmtd(s: str, width: int, fillchar: str = ' ') -> str:
"""
SGR-aware implementation of str.rjust().
.. seealso:: ljust_fmtd
.. note:: blabla
.. todo:: blabla
"""
sanitized = ReplaceSGR().apply(s)
fill_len = max(0, width - len(sanitized))
if fill_len == 0:
return s
right_fill_len = fill_len // 2
left_fill_len = fill_len - right_fill_len
return (fillchar * left_fill_len) + s + (fillchar * right_fill_len)
75 changes: 75 additions & 0 deletions pytermor/util/prefixed_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -----------------------------------------------------------------------------
# pytermor [ANSI formatted terminal output toolset]
# (C) 2022 A. Shavykin <0.delameter@gmail.com>
# -----------------------------------------------------------------------------
from __future__ import annotations

from dataclasses import dataclass
from typing import List

from . import fmt_auto_float


@dataclass
class PrefixedUnitFmtPreset:
"""
Default settings are suitable for formatting sizes in bytes (
*mcoef* =1024, prefixes are k, M, G, T etc.)
*max_value_len* cannot effectively be less than 3, at least
as long as *prefix_coef* =1024, because there is no way for
method to insert into output more digits than it can shift
back using multiplier (/divider) coefficient and prefixed units.
"""
max_value_len: int = 5
expand_to_max: bool = False
mcoef: float = 1024.0
unit: str|None = 'b'
unit_separator: str|None = ' '
prefixes: List[str] = None


FMT_PRESET_DEFAULT_KEY = 8
FMT_PRESETS = {FMT_PRESET_DEFAULT_KEY: PrefixedUnitFmtPreset()}

FMT_PREFIXES_DEFAULT = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z']


def fmt_prefixed_unit(value: int, preset: PrefixedUnitFmtPreset = None) -> str:
"""
Format *value* using *preset* settings. The main idea of this method
is to fit into specified string length as much significant digits as it's
theoretically possible, using increasing coefficients and unit prefixes to
indicate power.
:param value: input value
:param preset: formatter settings
:return: formatted value
"""
if preset is None:
preset = FMT_PRESETS[FMT_PRESET_DEFAULT_KEY]
value = max(0, value)

def iterator(_value: float) -> str:
prefixes = preset.prefixes if preset.prefixes else FMT_PREFIXES_DEFAULT
for unit_idx, unit_prefix in enumerate(prefixes):
unit = preset.unit if preset.unit else ""
unit_separator = preset.unit_separator if preset.unit_separator else ""
unit_full = f'{unit_prefix}{unit}'

if _value >= preset.mcoef:
_value /= preset.mcoef
continue

num_str = fmt_auto_float(_value, preset.max_value_len, (unit_idx == 0))
return f'{num_str}{unit_separator}{unit_full}'

# no more prefixes left
return f'{_value!r:{preset.max_value_len}.{preset.max_value_len}}{preset.unit_separator or ""}' + \
'?' * max([len(p) for p in prefixes]) + \
(preset.unit or "")

result = iterator(value)
if not preset.expand_to_max:
result = result.strip()
return result
Loading

0 comments on commit e026643

Please sign in to comment.