Source code for pisa.utils.format

# -*- coding: utf-8 -*-

"""
Utilities for interpreting and returning formatted strings.
"""


from __future__ import absolute_import, division, print_function

from collections.abc import Iterable, Sequence
from collections import OrderedDict
import decimal
from numbers import Integral, Number
import os
import re
import time

import numpy as np
import uncertainties

from pisa import FTYPE, ureg
from pisa.utils.flavInt import NuFlavIntGroup
from pisa.utils.log import Levels, logging, set_verbosity


__all__ = [
    'WHITESPACE_RE',
    'NUMBER_RESTR',
    'NUMBER_RE',
    'HRGROUP_RESTR',
    'HRGROUP_RE',
    'IGNORE_CHARS_RE',
    'TEX_BACKSLASH_CHARS',
    'TEX_SPECIAL_CHARS_MAPPING',
    'SI_PREFIX_TO_ORDER_OF_MAG',
    'ORDER_OF_MAG_TO_SI_PREFIX',
    'BIN_PREFIX_TO_POWER_OF_1024',
    'POWER_OF_1024_TO_BIN_PREFIX',
    'split',
    'arg_str_seq_none',
    'arg_to_tuple',
    'hr_range_formatter',
    'test_hr_range_formatter',
    'list2hrlist',
    'test_list2hrlist',
    'hrlist2list',
    'hrlol2lol',
    'hrbool2bool',
    'engfmt',
    'text2tex',
    'tex_join',
    'tex_dollars',
    'default_map_tex',
    'is_tex',
    'int2hex',
    'hash2hex',
    'strip_outer_dollars',
    'strip_outer_parens',
    'make_valid_python_name',
    'sep_three_tens',
    'format_num',
    'test_format_num',
    'timediff',
    'test_timediff',
    'timestamp',
    'test_timestamp',
]


__author__ = 'J.L. Lanfranchi'

__license__ = '''Copyright (c) 2014-2020, The IceCube Collaboration

 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.'''


WHITESPACE_RE = re.compile(r'\s')
NUMBER_RESTR = r'((?:-|\+){0,1}[0-9.]+(?:e(?:-|\+)[0-9.]+){0,1})'
"""RE str for matching signed, unsigned, and sci.-not. ("1e10") numbers."""

NUMBER_RE = re.compile(NUMBER_RESTR, re.IGNORECASE)
"""Regex for matching signed, unsigned, and sci.-not. ("1e10") numbers."""

# Optional range, e.g., --10 (which means "to negative 10"); in my
# interpretation, the "to" number should be *INCLUDED* in the list
# If there's a range, optional stepsize, e.g., --10 (which means
# "to negative 10")
HRGROUP_RESTR = (
    NUMBER_RESTR
    + r'(?:-' + NUMBER_RESTR
    + r'(?:\:' + NUMBER_RESTR + r'){0,1}'
    + r'){0,1}'
)
HRGROUP_RE = re.compile(HRGROUP_RESTR, re.IGNORECASE)

# Characters to ignore are anything EXCEPT the characters we use
# (the caret ^ inverts the set in the character class)
IGNORE_CHARS_RE = re.compile(r'[^0-9e:.,;+-]', re.IGNORECASE)

TEX_BACKSLASH_CHARS = '%$#_{}'
TEX_SPECIAL_CHARS_MAPPING = {
    '~': r'\textasciitilde',
    '^': r'\textasciicircum',
    ' ': r'\;',
    'sin': r'\sin',
    'cos': r'\cos',
    'tan': r'\tan',
    #'sqrt': r'\sqrt{\,}'
    #'sqrt': r'\surd'
}

ORDER_OF_MAG_TO_SI_PREFIX = OrderedDict([
    (-24, 'y'),
    (-21, 'z'),
    (-18, 'a'),
    (-15, 'f'),
    (-6, 'μ'),
    (-3, 'm'),
    (-9, 'n'),
    (-12, 'p'),
    (0, ''),
    (3, 'k'),
    (6, 'M'),
    (9, 'G'),
    (12, 'T'),
    (15, 'P'),
    (18, 'E'),
    (21, 'Z'),
    (24, 'Y')
])
"""Mapping of powers-of-10 to SI prefixes (orders-of-magnitude)"""

SI_PREFIX_TO_ORDER_OF_MAG = OrderedDict()
"""Mapping of SI prefixes to powers-of-10"""
for K, V in ORDER_OF_MAG_TO_SI_PREFIX.items():
    SI_PREFIX_TO_ORDER_OF_MAG[V] = K
# Allow "u" to map to -6 (micro) as well
SI_PREFIX_TO_ORDER_OF_MAG['u'] = -6

POWER_OF_1024_TO_BIN_PREFIX = OrderedDict([
    (0, ''),
    (1, 'Ki'),
    (2, 'Mi'),
    (3, 'Gi'),
    (4, 'Ti'),
    (5, 'Pi'),
    (6, 'Ei'),
    (7, 'Zi'),
    (8, 'Yi')
])
"""Mapping from powers-of-1024 to binary prefixes"""

BIN_PREFIX_TO_POWER_OF_1024 = OrderedDict()
"""Mapping from binary prefixes to powerorders-of-1024"""
for K, V in POWER_OF_1024_TO_BIN_PREFIX.items():
    BIN_PREFIX_TO_POWER_OF_1024[V] = K


[docs] def split(string, sep=',', force_case=None, parse_func=None): """Parse a string containing a separated list. * Before splitting the list, the string has extraneous whitespace removed from either end. * The strings that result after the split can have their case forced or be left alone. * Whitespace surrounding (but not falling between non-whitespace) in each resulting string is removed. * After all of the above, the value can be parsed further by a user-supplied `parse_func`. Note that repeating a separator without intervening values yields empty-string values. Parameters ---------- string : string The string to be split sep : string Separator to look for force_case : None, 'lower', or 'upper' Whether to force the case of the resulting items: None does not change the case, while 'lower' or 'upper' change the case. parse_func : None or callable If a callable is supplied, each item in the list, after the basic parsing, is processed by `parse_func`. Returns ------- lst : list of objects The types of the items in the list depend upon `parse_func` if it is supplied; otherwise, all items are strings. Examples -------- >>> print(split(' One, TWO, three ', sep=',', force_case='lower')) ['one', 'two', 'three'] >>> print(split('One:TWO:three', sep=':')) ['One', 'TWO', 'three'] >>> print(split('one two three', sep=' ')) ['one', '', 'two', '' , 'three'] >>> print(split('1 2 3', sep=' ', parse_func=int)) [1, 2, 3] >>> from ast import literal_eval >>> print(split('True; False; None; (1, 2, 3)', sep=',', >>> parse_func=literal_eval)) [True, False, None, (1, 2, 3)] """ funcs = [] if force_case == 'lower': funcs.append(str.lower) elif force_case == 'upper': funcs.append(str.upper) if parse_func is not None: if not callable(parse_func): raise TypeError('`parse_func` must be callable; got %s instead.' % type(parse_func)) funcs.append(parse_func) if not funcs: aggfunc = lambda x: x elif len(funcs) == 1: aggfunc = funcs[0] elif len(funcs) == 2: aggfunc = lambda x: funcs[1](funcs[0](x)) return [aggfunc(x.strip()) for x in str.split(str(string).strip(), sep)]
[docs] def arg_str_seq_none(inputs, name): """Simple input handler. Parameters ---------- inputs : None, string, or iterable of strings Input value(s) provided by caller name : string Name of input, used for producing a meaningful error message Returns ------- inputs : None, or list of strings Raises ------ TypeError if unrecognized type """ if isinstance(inputs, str): inputs = [inputs] elif isinstance(inputs, (Iterable, Sequence)): inputs = list(inputs) elif inputs is None: pass else: raise TypeError('Input %s: Unhandled type %s' % (name, type(inputs))) return inputs
[docs] def arg_to_tuple(arg): """Convert `arg` to a tuple: None becomes an empty tuple, an isolated string becomes a tuple containing that string, and any iterable or sequence is simply converted into a tuple. Parameters ---------- arg : str, sequence of str, iterable of str, or None Returns ------- arg_tup : tuple of str """ if arg is None: arg = tuple() elif isinstance(arg, str): arg = (arg,) elif isinstance(arg, (Iterable, Sequence)): arg = tuple(arg) else: raise TypeError('Unhandled type {}, arg={}'.format(type(arg), arg)) return arg
# TODO: allow for scientific notation input to hr*2list, etc.
[docs] def hr_range_formatter(start, end, step): """Format a range (sequence) in a simple and human-readable format by specifying the range's starting number, ending number (inclusive), and step size. Parameters ---------- start, end, step : numeric Notes ----- If `start` and `end` are integers and `step` is 1, step size is omitted. The format does NOT follow Python's slicing syntax, in part because the interpretation is meant to differ; e.g., '0-10:2' includes both 0 and 10 with step size of 2 whereas 0:10:2 (slicing syntax) excludes 10 Numbers are converted to integers if they are equivalent for more compact display. Examples -------- >>> hr_range_formatter(start=0, end=10, step=1) '0-10' >>> hr_range_formatter(start=0, end=10, step=2) '0-10:2' >>> hr_range_formatter(start=0, end=3, step=8) '0-3:8' >>> hr_range_formatter(start=0.1, end=3.1, step=1.0) '0.1-3.1:1' """ if int(start) == start: start = int(start) if int(end) == end: end = int(end) if int(step) == step: step = int(step) if int(start) == start and int(end) == end and step == 1: return '{}-{}'.format(start, end) return '{}-{}:{}'.format(start, end, step)
[docs] def test_hr_range_formatter(): """Unit tests for hr_range_formatter""" logging.debug(str((hr_range_formatter(start=0, end=10, step=1)))) logging.debug(str((hr_range_formatter(start=0, end=10, step=2)))) logging.debug(str((hr_range_formatter(start=0, end=3, step=8)))) logging.debug(str((hr_range_formatter(start=0.1, end=3.1, step=1.0)))) logging.info('<< PASS : test_hr_range_formatter >>')
[docs] def list2hrlist(lst): """Convert a list of numbers to a compact and human-readable string. Parameters ---------- lst : sequence Notes ----- Adapted to make scientific notation work correctly from [1]. References ---------- [1] http://stackoverflow.com/questions/9847601 user Scott B's adaptation to Python 2 of Rik Poggi's answer to his question Examples -------- >>> list2hrlist([0, 1]) '0,1' >>> list2hrlist([0, 3]) '0,3' >>> list2hrlist([0, 1, 2]) '0-2' >>> utils.list2hrlist([0.1, 1.1, 2.1, 3.1]) '0.1-3.1:1' >>> list2hrlist([0, 1, 2, 4, 5, 6, 20]) '0-2,4-6,20' """ if isinstance(lst, Number): lst = [lst] lst = sorted(lst) rtol = np.finfo(FTYPE).resolution n = len(lst) result = [] scan = 0 while n - scan > 2: step = lst[scan + 1] - lst[scan] if not np.isclose(lst[scan + 2] - lst[scan + 1], step, rtol=rtol): result.append(str(lst[scan])) scan += 1 continue for j in range(scan+2, n-1): if not np.isclose(lst[j+1] - lst[j], step, rtol=rtol): result.append(hr_range_formatter(lst[scan], lst[j], step)) scan = j+1 break else: result.append(hr_range_formatter(lst[scan], lst[-1], step)) return ','.join(result) if n - scan == 1: result.append(str(lst[scan])) elif n - scan == 2: result.append(','.join(map(str, lst[scan:]))) return ','.join(result)
[docs] def test_list2hrlist(): """Unit tests for list2hrlist""" logging.debug(str((list2hrlist([0, 1])))) logging.debug(str((list2hrlist([0, 1, 2])))) logging.debug(str((list2hrlist([0.1, 1.1, 2.1, 3.1])))) logging.info('<< PASS : test_list2hrlist >>')
def _hrgroup2list(hrgroup): def isint(num): """Test whether a number is *functionally* an integer""" try: return int(num) == FTYPE(num) except ValueError: return False def num_to_float_or_int(num): """Return int if number is effectively int, otherwise return float""" try: if isint(num): return int(num) except (ValueError, TypeError): pass return FTYPE(num) # Strip all whitespace, brackets, parens, and other ignored characters from # the group string hrgroup = IGNORE_CHARS_RE.sub('', hrgroup) if (hrgroup is None) or (hrgroup == ''): return [] num_str = HRGROUP_RE.match(hrgroup).groups() range_start = num_to_float_or_int(num_str[0]) # If no range is specified, just return the number if num_str[1] is None: return [range_start] range_stop = num_to_float_or_int(num_str[1]) if num_str[2] is None: step_size = 1 if range_stop >= range_start else -1 else: step_size = num_to_float_or_int(num_str[2]) all_ints = isint(range_start) and isint(step_size) # Make an *INCLUSIVE* list (as best we can considering floating point mumbo # jumbo) n_steps = np.clip( np.floor(np.around( (range_stop - range_start)/step_size, decimals=12, )), a_min=0, a_max=np.inf ) lst = np.linspace(range_start, range_start + n_steps*step_size, n_steps+1) if all_ints: lst = lst.astype(int) return lst.tolist()
[docs] def hrlist2list(hrlst): """Convert human-readable string specifying a list of numbers to a Python list of numbers. Parameters ---------- hrlist : string Returns ------- lst : list of numbers """ groups = re.split(r'[,; _]+', WHITESPACE_RE.sub('', hrlst)) lst = [] if not groups: return lst for group in groups: lst.extend(_hrgroup2list(group)) return lst
[docs] def hrlol2lol(hrlol): """Convert a human-readable string specifying a list-of-lists of numbers to a Python list-of-lists of numbers. Parameters ---------- hrlol : string Human-readable list-of-lists-of-numbers string. Each list specification is separated by a semicolon, and whitespace is ignored. Refer to `hrlist2list` for list specification. Returns ------- lol : list-of-lists of numbers Examples -------- A single number evaluates to a list with a list with a single number. >>> hrlol2lol("1") [[1]] A sequence of numbers or ranges can be specified separated by commas. >>> hrlol2lol("1, 3.2, 19.8") [[1, 3.2, 19.8]] A range can be specified with a dash; default is a step size of 1 (or -1 if the end of the range is less than the start of the range); note that the endpoint is included, unlike slicing in Python. >>> hrlol2lol("1-3") [[1, 2, 3]] The range can go from or to a negative number, and can go in a negative direction. >>> hrlol2lol("-1 - -5") [[-1, -3, -5]] Multiple lists are separated by semicolons, and parentheses and brackets can be used to make it easier to understand the string. >>> hrlol2lol("1 ; 8 ; [(-10 - -8:2), 1]") [[1], [8], [-10, -8, 1]] Finally, all of the above can be combined. >>> hrlol2lol("1.-3.; 9.5-10.6:0.5,3--1:-1; 12.5-13:0.8") [[1, 2, 3], [9.5, 10.0, 10.5, 3, 2, 1, 0, -1], [12.5]] """ supergroups = re.split(r'[;]+', hrlol) return [hrlist2list(group) for group in supergroups]
[docs] def hrbool2bool(s): """Convert a string that a user might input to indicate a boolean value of either True or False and convert to the appropriate Python bool. * Note first that the case used in the string is ignored * 't', 'true', '1', 'yes', and 'one' all map to True * 'f', 'false', '0', 'no', and 'zero' all map to False Parameters ---------- s : string Returns ------- b : bool """ s = str(s).strip() if s.lower() in ['t', 'true', '1', 'yes', 'one']: return True if s.lower() in ['f', 'false', '0', 'no', 'zero']: return False raise ValueError('Could not parse input "%s" to bool.' % s)
[docs] def engfmt(n, sigfigs=3, decimals=None, sign_always=False): """Format number as string in engineering format (10^(multiples-of-three)), including the most common metric prefixes (from atto to Exa). Parameters ---------- n : scalar Number to be formatted sigfigs : int >= 0 Number of significant figures to limit the result to; default=3. decimals : int or None Number of decimals to display (zeros filled out as necessary). If None, `decimals` is automatically determined by the magnitude of the significand and the specified `sigfigs`. sign_always : bool Prefix the number with "+" sign if number is positive; otherwise, only negative numbers are prefixed with a sign ("-") """ if isinstance(n, ureg.Quantity): units = n.units n = n.magnitude else: units = ureg.dimensionless # Logs don't like negative numbers... sign = np.sign(n) n *= sign mag = int(np.floor(np.log10(n))) pfx_mag = int(np.floor(np.log10(n)/3.0)*3) if decimals is None: decimals = sigfigs-1 - (mag-pfx_mag) decimals = int(np.clip(decimals, a_min=0, a_max=np.inf)) round_to = decimals if sigfigs is not None: round_to = sigfigs-1 - (mag-pfx_mag) scaled_rounded = np.round(n/10.0**pfx_mag, round_to) sign_str = '' if sign_always and sign > 0: sign_str = '+' num_str = sign_str + format(sign*scaled_rounded, '.'+str(decimals)+'f') # Very large or small quantities have their order of magnitude displayed # by printing the exponent rather than showing a prefix; due to my # inability to strip off prefix in Pint quantities (and attach my own # prefix), just use the "e" notation. if pfx_mag not in ORDER_OF_MAG_TO_SI_PREFIX or not units.dimensionless: if pfx_mag == 0: return str.strip('{0:s} {1:~} '.format(num_str, units)) return str.strip('{0:s}e{1:d} {2:~} '.format(num_str, pfx_mag, units)) # Dimensionless quantities are treated separately since Pint apparently # can't handle prefixed-dimensionless (e.g., simply "1 k", "2.2 M", etc., # with no units attached). #if units.dimensionless: return '{0:s} {1:s}'.format(num_str, ORDER_OF_MAG_TO_SI_PREFIX[pfx_mag])
def append_results(results_dict, result_dict): for key, val in result_dict.items(): if key in results_dict: results_dict[key].append(val) else: results_dict[key] = [val] def ravel_results(results): for key, val in results.items(): if hasattr(val[0], 'm'): results[key] = np.array([v.m for v in val]) * val[0].u # TODO: mathrm vs. rm?
[docs] def text2tex(txt): """Convert common characters so they show up the same as TeX""" if txt is None: return '' if is_tex(txt): return strip_outer_dollars(txt) nfig = NuFlavIntGroup(txt) if nfig: return nfig.tex for c in TEX_BACKSLASH_CHARS: txt = txt.replace(c, r'\%s'%c) for c, v in TEX_SPECIAL_CHARS_MAPPING.items(): txt = txt.replace(c, '{%s}'%v) # A single character is taken to be a variable name, and so do not make # roman, just wrap in braces (to avoid interference with other characters) # and return if len(txt) == 1: return '%s' % txt return r'{\rm %s}' % txt
[docs] def tex_join(sep, *args): """Join TeX-formatted strings together into one, each separated by `sep`. Also, this strips surrounding '$' from each string before joining.""" strs = [strip_outer_dollars(text2tex(a)) for a in args if a is not None and a != ''] if not strs: return '' return str.join(sep, strs)
[docs] def tex_dollars(s): stripped = strip_outer_dollars(s) out_lines = [] for line in stripped.split('\n'): stripped_line = strip_outer_dollars(line) if stripped_line == '': out_lines.append('') else: out_lines.append('$%s$' % stripped_line) return '\n'.join(out_lines)
[docs] def is_tex(s): if s is None: return False for c in TEX_BACKSLASH_CHARS: if '\\'+c in s: return True for seq in TEX_SPECIAL_CHARS_MAPPING.values(): if seq in s: return True for seq in [r'\rm', r'\mathrm', r'\theta', r'\phi']: if seq in s: return True if strip_outer_dollars(s) != s: return True return False
[docs] def default_map_tex(map): if map.tex is None or map.tex == '': return r'{\rm %s}' % text2tex(map.name) return strip_outer_dollars(map.tex)
[docs] def int2hex(i, bits, signed): """Convert a signed or unsigned integer `bits` long to hexadecimal representation. As many hex characters are returned to fully specify any number `bits` in length regardless of the value of `i`. Parameters ---------- i : int The integer to be converted. Signed integers have a range of -2**(bits-1) to 2**(bits-1)-1), while unsigned integers have a range of 0 to 2**(bits-1). bits : int Number of bits long the representation is signed : bool Whether the number is a signed integer; this is dependent upon the representation used for numbers, and _not_ whether the value `i` is positive or negative. Returns ------- h : string of length ceil(bits/4.0) since it takes this many hex characters to represent a number `bits` long. """ if signed: i = 2**63 + i assert i >= 0 h = hex(i)[2:].replace('L', '') return h.rjust(int(np.ceil(bits/4.0)), '0')
[docs] def hash2hex(hash, bits=64): """Convert a hash value to its string hexadecimal representation. Parameters ---------- hash : integer or string bits : integer > 0 Returns ------- hash : string """ if isinstance(hash, str): assert len(hash) == int(np.ceil(bits/4.0)) hex_hash = hash elif isinstance(hash, int): hex_hash = int2hex(hash, bits=bits, signed=True) else: raise TypeError('Unhandled `hash` type %s' %type(hash)) return hex_hash
[docs] def strip_outer_dollars(value): """Strip surrounding dollars signs from TeX string, ignoring leading and trailing whitespace""" if value is None: return '{}' value = value.strip() m = re.match(r'^\$(.*)\$$', value) if m is not None: value = m.groups()[0] return value
[docs] def strip_outer_parens(value): """Strip parentheses surrounding a string, ignoring leading and trailing whitespace""" if value is None: return '' value = value.strip() m = re.match(r'^\{\((.*)\)\}$', value) if m is not None: value = m.groups()[0] m = re.match(r'^\((.*)\)$', value) if m is not None: value = m.groups()[0] return value
# TODO: this is relatively slow (and is called in constructors that are used # frequently, e.g. OneDimBinning, MultiDimBinning); can we speed it up any? RE_INVALID_CHARS = re.compile('[^0-9a-zA-Z_]') RE_LEADING_INVALID = re.compile('^[^a-zA-Z_]+')
[docs] def make_valid_python_name(name): """Make a name a valid Python identifier. From user Triptych at http://stackoverflow.com/questions/3303312 """ # Remove invalid characters name = RE_INVALID_CHARS.sub('', name) # Remove leading characters until we find a letter or underscore name = RE_LEADING_INVALID.sub('', name) return name
[docs] def sep_three_tens(strval, direction, sep=None): """Insert `sep` char into sequence of chars `strval`. Parameters ---------- strval : sequence of chars or string Sequence of chars into which to insert the separator direction : string, one of {'left', 'right'} Use 'left' for left of the decimal, and 'right' for right of the decimal sep : None or char Separator to insert Returns ------- formatted : list of chars """ if not sep: return strval direction = direction.strip().lower() assert direction in ('left', 'right'), direction formatted = [] if direction == 'left': indices = tuple(range(len(strval)-1, -1, -1)) edge_indices = (indices[0], indices[-1]) delta = len(strval)-1 for c_num in indices: formatted = [strval[c_num]] + formatted if (((delta-c_num)+1) % 3 == 0) and c_num not in edge_indices: formatted = [sep] + formatted return formatted indices = tuple(range(len(strval))) edge_indices = (indices[0], indices[-1]) for c_num in indices: formatted = formatted + [strval[c_num]] if ((c_num+1) % 3 == 0) and (c_num not in edge_indices): formatted = formatted + [sep] return formatted
[docs] def format_num( value, sigfigs=None, precision=None, fmt=None, sci_thresh=(6, -4), exponent=None, inf_thresh=np.infty, trailing_zeros=False, always_show_sign=False, decstr='.', thousands_sep=None, thousandths_sep=None, left_delimiter=None, right_delimiter=None, expprefix=None, exppostfix=None, nanstr='nan', infstr='inf', ): r"""Fine-grained control over formatting a number as a string. Parameters ---------- value : numeric The number to be formatted. sigfigs : int > 0, optional Use up to this many significant figures for displaying a number. You can use either `sigfigs` or `precision`, but not both. If neither are specified, default is to set `sigfigs` to 8. See also `trailing_zeros`. precision : float, optional Round `value` to a precision the same as the order of magnitude of `precision`. You can use either `precision` or `sigfigs`, but not both. If neither is specified, default is to set `sigfigs` to 8. See also `trailing_zeros`. fmt : None or one of {'sci', 'eng', 'sipre', 'binpre', 'full'}, optional Force a particular format to be used:: * None allows the `value` and what is passed for `sci_thresh` and `exponent` to decide whether or not to use scientific notation * 'sci' forces scientific notation * 'eng' uses the engineering convention of powers divisible by 3 (10e6, 100e-9, etc.) * 'sipre' uses powers divisible by 3 but uses SI prefixes (e.g. k, M, G, etc.) instead of displaying the exponent * 'binpre' uses powers of 1024 and uses IEC prefixes (e.g. Ki, Mi, Gi, etc.) instead displaying the exponent * 'full' forces printing all digits left and/or right of the decimal to display the number (no exponent notation or SI/binary prefix will be used) Note that the display of NaN and +/-inf are unaffected by `fmt`. exponent : None, integer, or string, optional Force the number to be scaled with respect to this exponent. If a string prefix is passed and `fmt` is None, then the SI prefix or binary prefix will be used for the number. E.g., ``exponent=3`` would cause the number 1 to be expressed as ``'0.001e3'`, while ``exponent='k'`` would cause it to be displayed as ``'1 m'``. Both 'μ' and 'u' are accepted to mean "micro". A non-``None`` value for `exponent` forces some form of scientific/engineering notation, so `fmt` cannot be ``'full'`` in this case. Finally, if `fmt` is ``'binpre'`` then `exponent` is applied to 1024. I.e., 1 maps to kibi (Ki), 2 maps to mebi (Mi), etc. sci_thresh : sequence of 2 integers When to switch to scientific notation. The first integer is the order of magnitude of `value` at or above which scientific notation will be used. The second integer indicates the order of magnitude at or below which the most significant digit falls for scientific notation to be used. E.g., ``sci_thresh=(3, -3)`` means that numbers in the ones-of-thousands or greater OR numbers in the ones-of-thousandths or less will be displayed using scientific notation. Note that `fmt`, if not None, overrides this behavior. Default is (10,-5). inf_thresh : numeric, optional Numbers whose magnitude is equal to or greater than this threhshold are considered infinite and therefore displayed using `infstr` (possibly including a sign, as appropriate). Default is np.inf. trailing_zeros : bool, optional Whether to display all significant figures specified by `sigfigs`, even if this results in trailing zeros. Default is False. always_show_sign : bool, optional Always show a sign, whether number is positive or negative, and whether exponent (if present) is positive or negative. Default is False. decstr : string, optional Separator to use for the decimal point. E.g. ``decstr='.'`` or ``decstr=','`` for mthe most common cases, but this can also be used in TeX tables for alignment on decimal points via ``decstr='&.&'``. Default is '.'. thousands_sep : None or string, optional Separator to use between thousands, e.g. ``thousands_sep=','`` to give results like ``'1,000,000'``, or ```thousands_sep=r'\,'`` for TeX formatting with small spaces between thousands. Default is None. thousandths_sep : None or string, optional Separator to use between thousandthss. Default is None. left_delimiter, right_delimiter : None or string, optional Strings to delimit the left and right sides of the resulting string. E.g. ``left_delimiter='${'`` and ``right_delimiter='}$'`` could be used to delimit TeX-formatted strings, such that a number is displayed, e.g., as ``r'${1\times10^3}$'``. Defaults are None for both. expprefix, exppostfix : None or string, optional E.g. use `expprefix='e'` for simple "e" scientific notation ("1e3"), or use `expprefix=r'\times10^{'` and `exppostfix=r'}' for TeX-formatted scientific notation. Use a space (or tex equivalent) for binary and SI prefixes. If scientific notation is to be used, `expprefix` defaults to 'e'. If either SI or binary prefixes are to be used, `expprefix` defaults to ' ' (space). In any case, `exppostfix` defaults to None. nanstr : string, optional Not-a-number (NaN) values will be displayed using this string. Default is 'nan' (following the Numpy convention) infstr : string, optional Infinite values will be displayed using this string (note that the sign is prepended, as appropriate). Default is 'inf' (following the Numpy convention). Returns ------- formatted : string """ with decimal.localcontext() as context: # Ensure rounding behavior is same as that of Numpy context.rounding = decimal.ROUND_HALF_EVEN # Lots of comp precision to avoid floating point <--> decimal issues context.prec = 72 d_10 = decimal.Decimal('10') d_1024 = decimal.Decimal('1024') if sigfigs is None: if precision is None: sigfigs = 8 else: precision = decimal.Decimal(precision) order_of_precision = precision.adjusted() else: if precision is not None: raise ValueError('You cannot specify both `sigfigs` and' ' `precision`') if not isinstance(sigfigs, Integral): assert float(sigfigs) == int(sigfigs), \ '`sigfigs`=%s not an int' % sigfigs sigfigs = int(sigfigs) assert sigfigs > 0, '`sigfigs`=%s is not > 0' % sigfigs if sci_thresh[0] < sci_thresh[1]: raise ValueError( '(`sci_thresh[0]`=%s) must be >= (`sci_thresh[1]`=%s)' % sci_thresh ) assert all(isinstance(s, Integral) for s in sci_thresh), str(sci_thresh) if isinstance(fmt, str): fmt = fmt.strip().lower() assert fmt is None or fmt in ('sci', 'eng', 'sipre', 'binpre', 'full') if fmt == 'full': assert exponent is None if exponent is not None: if fmt in ('eng', 'sipre'): if (exponent not in SI_PREFIX_TO_ORDER_OF_MAG and exponent not in ORDER_OF_MAG_TO_SI_PREFIX): raise ValueError( 'For `fmt`="{}", `exponent` is {}, but must either be' ' an SI prefix {} or a power of 10 corresponding to' ' these {}.'.format(fmt, exponent, SI_PREFIX_TO_ORDER_OF_MAG.keys(), ORDER_OF_MAG_TO_SI_PREFIX.keys()) ) elif fmt == 'binpre': if (exponent not in BIN_PREFIX_TO_POWER_OF_1024 and exponent not in POWER_OF_1024_TO_BIN_PREFIX): raise ValueError( 'For `fmt`="{}", `exponent` is {}, but must either be' ' an IEC binary prefix {} or a power of 1024' ' corresponding to these {}.'.format( fmt, exponent, BIN_PREFIX_TO_POWER_OF_1024.keys(), POWER_OF_1024_TO_BIN_PREFIX.keys() ) ) if (not isinstance(exponent, str) and not isinstance(exponent, Integral)): assert float(exponent) == int(exponent) exponent = int(exponent) # TODO: include uncertainties and/or units in final formatted string # TODO: scale out SI prefix if `value` is a Pint Quantity # Strip off units, if present units = None quantity_info = None if isinstance(value, ureg.Quantity): units = value.units if value.units != ureg.dimensionless else None quantity_info = value.as_tuple() value = value.magnitude # Strip off uncertainty, if present stddev = None if isinstance(value, uncertainties.UFloat): stddev = value.std_dev value = value.nominal_value # In case `value` is a singleton array if isinstance(value, np.ndarray): value = value.item() # Fill in empty strings where None might be passed in to mean the same thousands_sep = '' if thousands_sep is None else thousands_sep thousandths_sep = '' if thousandths_sep is None else thousandths_sep left_delimiter = '' if left_delimiter is None else left_delimiter right_delimiter = '' if right_delimiter is None else right_delimiter exppostfix = '' if exppostfix is None else exppostfix # NOTE: expprefix defaults depend on the display mode, so are set later if np.isnan(value): return left_delimiter + nanstr + right_delimiter if np.isneginf(value) or value <= -inf_thresh: return left_delimiter + '-' + infstr + right_delimiter # NOTE: ``isinf`` check must come _after_ ``neginf`` check since # ``isinf`` returns ``True`` for both -inf and +inf if np.isinf(value) or value >= inf_thresh: if always_show_sign: sign = '+' else: sign = '' return left_delimiter + sign + infstr + right_delimiter if isinstance(value, Integral): value = decimal.Decimal(value) else: value = decimal.Decimal.from_float(float(value)) order_of_mag = value.adjusted() # Get the sign from the full precision `value`, before rounding sign = '' if value < 0: sign = '-' elif value > 0: sign = '+' # If no value passed for `fmt`, infer the format from the # exponent (if it's a binary or SI prefix) OR the order of magnitude of # the number w.r.t. `sci_thresh`. if fmt is None: if isinstance(exponent, str): if exponent in BIN_PREFIX_TO_POWER_OF_1024: fmt = 'binpre' elif exponent in SI_PREFIX_TO_ORDER_OF_MAG: fmt = 'sipre' else: raise ValueError('`exponent`="%s" is not a valid SI or' ' binary prefix' % exponent) elif exponent is None: if (order_of_mag >= sci_thresh[0] or order_of_mag <= sci_thresh[1]): fmt = 'sci' else: fmt = 'full' else: fmt = 'sci' # Define `exponent` where appropriate, and calculate `scaled_value` to # account for the exponent, if there is one. scale = 1 if exponent is None: if fmt == 'sci': exponent = order_of_mag scale = 1 / d_10**exponent elif fmt in ('eng', 'sipre'): exponent = (order_of_mag // 3) * 3 scale = 1 / d_10**exponent if fmt == 'sipre': exponent = ORDER_OF_MAG_TO_SI_PREFIX[exponent] elif fmt == 'binpre': if value < 0: raise ValueError('Binary prefix valid only for value >= 0') elif value == 0: exponent = 0 scale = 1 else: exponent = value.ln() // d_1024.ln() scale = 1 / d_1024**exponent exponent = POWER_OF_1024_TO_BIN_PREFIX[exponent] elif exponent in BIN_PREFIX_TO_POWER_OF_1024: scale = 1 / d_1024**BIN_PREFIX_TO_POWER_OF_1024[exponent] elif exponent in SI_PREFIX_TO_ORDER_OF_MAG: scale = 1 / d_10**SI_PREFIX_TO_ORDER_OF_MAG[exponent] else: scale = 1 / d_10**exponent scaled_value = scale * value if sigfigs is not None: leastsig_dig = scaled_value.adjusted() - (sigfigs - 1) quantize_at = decimal.Decimal('1e%d' % leastsig_dig).normalize() else: # only other case is that precision is specified quantize_at = (d_10**order_of_precision * scale).normalize() leastsig_dig = quantize_at.adjusted() rounded = scaled_value.quantize(quantize_at) # Eliminate trailing zeros in the Decimal representation if not trailing_zeros: rounded = rounded.normalize() dec_tup = rounded.as_tuple() mantissa_digits = dec_tup.digits decimal_position = dec_tup.exponent # Does the number underflow, making it effectively 0? underflow = False if sigfigs is not None: if len(mantissa_digits) + decimal_position < -sigfigs: underflow = True decimal_position = -(sigfigs - 1) mantissa_digits = (0,) else: # `precision` is specified if order_of_mag < order_of_precision: underflow = True mantissa_digits = (0,) decimal_position = leastsig_dig n_digits = len(mantissa_digits) chars = [str(d) for d in mantissa_digits] if decimal_position > 0: chars += ['0']*decimal_position chars = sep_three_tens(chars, direction='left', sep=thousands_sep) elif decimal_position < 0: if abs(decimal_position) >= n_digits: chars = ( ['0', decstr] + sep_three_tens( ['0']*(-decimal_position - n_digits) + chars, direction='right', sep=thousandths_sep ) ) else: chars = ( sep_three_tens(chars[:decimal_position], direction='left', sep=thousands_sep) + [decstr] + sep_three_tens(chars[decimal_position:], direction='right', sep=thousandths_sep) ) num_str = ''.join(chars) if always_show_sign or sign == '-' or underflow: num_str = sign + num_str if exponent is not None: if expprefix is None: if fmt in ('sci', 'eng'): expprefix = 'e' elif fmt in ('sipre', 'binpre'): expprefix = ' ' else: expprefix = '' if not isinstance(exponent, str): if fmt == 'sipre': exponent = ORDER_OF_MAG_TO_SI_PREFIX[exponent] elif fmt == 'binpre': exponent = POWER_OF_1024_TO_BIN_PREFIX[exponent] if isinstance(exponent, str): num_str += expprefix + exponent + exppostfix else: if exponent < 0: exp_sign = '' elif always_show_sign: exp_sign = '+' else: exp_sign = '' num_str += expprefix + exp_sign + str(exponent) + exppostfix return left_delimiter + num_str + right_delimiter
def format_times(times, nindent_detailed=0, detailed=False, **format_num_kwargs): """Report statistics derived from a sample of run times, whose size may represent the number of calls to some function, using a custom number format. Parameters ---------- times : Sequence of float Sequence of run times nindent_detailed : int, optional Number of spaces for indentation of detailed info detailed : bool, default False Whether to output every individual run time also **format_num_kwargs : dict, optional Arguments to `format_num`: refer to its documentation for the list of all possible arguments. Returns ------- formatted : str """ assert isinstance(times, Sequence) tot = np.sum(times) n = len(times) if n == 0: return 'n calls: 0' ave = format_num(tot/n, **format_num_kwargs) tot = format_num(tot, **format_num_kwargs) max_time = format_num(np.max(times), **format_num_kwargs) min_time = format_num(np.min(times), **format_num_kwargs) formatted = f'Total time (s): {tot}, n calls: {n}' if n > 1: formatted += ( f', time/call (s): mean {ave}, max. {max_time}, min. {min_time}' ) if detailed: assert isinstance(nindent_detailed, int) formatted += '\n' + ' ' * nindent_detailed + 'Individual runs: ' for i, t in enumerate(times): formatted += '%i: %s s, ' % ( i, format_num(t, **format_num_kwargs) ) return formatted
[docs] def test_format_num(): """Unit tests for the `format_num` function""" # sci_thresh v = format_num(100, sci_thresh=(3, -3)) assert v == '100' v = format_num(1000, sci_thresh=(3, -3)) assert v == '1e3' v = format_num(0.01, sci_thresh=(3, -3)) assert v == '0.01' v = format_num(0.001, sci_thresh=(3, -3)) assert v == '1e-3' # trailing_zeros v = format_num(0.00010001, sigfigs=6, exponent=None, trailing_zeros=True) assert v == '1.00010e-4', v v = format_num(0.00010001, sigfigs=6, exponent=None, trailing_zeros=False) assert v == '1.0001e-4', v v = format_num(0.00010001, sigfigs=6, exponent=None, trailing_zeros=False, sci_thresh=(7, -8)) assert v == '0.00010001', v v = format_num(0.00010001, sigfigs=6, exponent=None, trailing_zeros=True, sci_thresh=(7, -20)) assert v == '0.000100010', v # sigfigs and trailing_zeros v = format_num(1, sigfigs=5, exponent=None, trailing_zeros=True) assert v == '1.0000', v v = format_num(1, sigfigs=5, exponent=None, trailing_zeros=False) assert v == '1', v v = format_num(16, sigfigs=5, exponent=None, trailing_zeros=False) assert v == '16', v v = format_num(160000, sigfigs=5, exponent=None, trailing_zeros=False) assert v == '160000', v v = format_num(123456789, sigfigs=15, exponent=None, trailing_zeros=False, sci_thresh=(20, -20)) assert v == '123456789', v v = format_num(1.6e6, sigfigs=5, exponent=None, trailing_zeros=False) assert v == '1.6e6', v # precision v = format_num(1.2345, precision=1e0, trailing_zeros=True) assert v == '1', v v = format_num(1.2345, precision=1e-1, trailing_zeros=True) assert v == '1.2', v # exponent v = format_num(1e6, sigfigs=5, exponent='k', trailing_zeros=False) assert v == '1000 k', v v = format_num(0.00134, sigfigs=5, exponent='m', trailing_zeros=False) assert v == '1.34 m', v v = format_num(1024, exponent='Ki') assert v == '1 Ki', v v = format_num(1024*1000, exponent='Ki') assert v == '1000 Ki', v v = format_num(1024**2, exponent='Mi') assert v == '1 Mi', v # displaying zero v = format_num(0, sigfigs=5, exponent=4, trailing_zeros=True) assert v == '0.0000e4', v v = format_num(0, sigfigs=5, exponent=4, trailing_zeros=False) assert v == '0e4', v v = format_num(0, sigfigs=5, exponent=None, trailing_zeros=True) assert v == '0.0000', v v = format_num(0, sigfigs=5, exponent=None, trailing_zeros=False) assert v == '0' v = format_num(0, sigfigs=5, fmt='sci') assert v == '0e0' v = format_num(0, sigfigs=5, fmt='eng') assert v == '0e0' v = format_num(0, sigfigs=5, fmt='sipre') assert v == '0 ' v = format_num(0, sigfigs=5, fmt='binpre') assert v == '0 ' v = format_num(0, sigfigs=5, fmt='full') assert v == '0' # exponent + sigfigs or precision causes underflow v = format_num(-0.00010001, sigfigs=6, exponent=4, trailing_zeros=True) assert v == '-0.00000e4', v v = format_num(0.00010001, sigfigs=6, exponent=4, trailing_zeros=True) assert v == '+0.00000e4', v v = format_num(-0.00010001, precision=1e-3, exponent=4, trailing_zeros=True) assert v == '-0.0000000e4', v v = format_num(0.00010001, precision=1e-3, exponent=4, trailing_zeros=True) assert v == '+0.0000000e4', v # exponent + precision, check sigfigs and trailing zeros... # zeros... v = format_num(-0.00010001, precision=1e-3, exponent=4, trailing_zeros=True) assert v == '-0.0000000e4', v v = format_num(0.00010001, precision=1e-3, exponent=4, trailing_zeros=True) assert v == '+0.0000000e4', v # rounding at least sig digit v = format_num(-0.00015001, precision=1e-4, exponent=4, trailing_zeros=True) assert v == '-0.00000002e4', v v = format_num(0.00015001, precision=1e-4, exponent=4, trailing_zeros=True) assert v == '0.00000002e4', v # trailing zeros v = format_num(-0.015001, precision=1e-4, exponent=4, trailing_zeros=True) assert v == '-0.00000150e4', v v = format_num(0.015001, precision=1e-4, exponent=4, trailing_zeros=True) assert v == '0.00000150e4', v # Test thousands_sep and thousandths_sep v = format_num(1000.0001, sigfigs=10, trailing_zeros=True, thousands_sep=',', thousandths_sep=' ') assert v == '1,000.000 100', v # Test specials: +/-inf, nan v = format_num(np.nan, sigfigs=10, trailing_zeros=True, thousands_sep=',', thousandths_sep=' ') assert v == 'nan', v v = format_num(np.inf, infstr='INFINITY', always_show_sign=True) assert v == '+INFINITY', v v = format_num(-np.inf, infstr='INFINITY') assert v == '-INFINITY', v v = format_num(1000, inf_thresh=100) assert v == 'inf', v v = format_num(-1000, inf_thresh=100) assert v == '-inf', v # eng and sipre with exponent v = format_num(1000, exponent=6, precision=1e3) assert v == '0.001e6', v v = format_num(1000, exponent=6, precision=1e3, fmt='sipre') assert v == '0.001 M', v v = format_num(115e3, exponent=6, precision=1e5, fmt='sipre', trailing_zeros=True) assert v == '0.1 M', v v = format_num(115e3, exponent=6, precision=1e4, fmt='sipre', trailing_zeros=True) assert v == '0.12 M', v v = format_num(115e3, exponent=6, precision=1e3, fmt='sipre', trailing_zeros=True) assert v == '0.115 M', v v = format_num(115e3, exponent=6, precision=1e2, fmt='sipre', trailing_zeros=True) assert v == '0.1150 M', v v = format_num(115e3, exponent=6, precision=1e1, fmt='sipre', trailing_zeros=True) assert v == '0.11500 M', v # TeX formatting (use exp{pre,post}fix, {left,right}_delimiter; # also fmt='eng', 'sipre', and 'binpre' v = format_num( value=12.5e3, sigfigs=4, trailing_zeros=True, fmt='eng', expprefix=r' \, \times 10^{', exppostfix='}', left_delimiter='${', right_delimiter='}$' ) assert v == r'${12.50 \, \times 10^{3}}$', v v = format_num( value=12.5e3, sigfigs=4, trailing_zeros=False, fmt='sipre', expprefix=r' \, {\rm ', exppostfix='}', left_delimiter='${', right_delimiter='}$' ) assert v == r'${12.5 \, {\rm k}}$', v v = format_num( value=12.5e3, sigfigs=4, trailing_zeros=False, fmt='binpre', expprefix=r' \, {\rm ', exppostfix='}', left_delimiter='${', right_delimiter='}$' ) assert v == r'${12.21 \, {\rm Ki}}$', v # fmt='full' v = format_num(12.5e10, sigfigs=4, trailing_zeros=False, fmt='full',) assert v == '125000000000', v # specify both fmt='full' AND exponent (should raise exception) try: v = format_num( 12.5e3, sigfigs=4, trailing_zeros=False, fmt='full', exponent=0 ) except AssertionError: pass else: assert False, '`fmt`="full" and `exponent` is defined' logging.info('<< PASS : test_format_num >>')
[docs] def timediff(dt_sec, hms_always=False, sec_decimals=3): """Smart string formatting for a time difference (in seconds) Parameters ---------- dt_sec : numeric Time difference, in seconds hms_always : bool * True Always display hours, minuts, and seconds regardless of the order- of-magnitude of dt_sec * False Display a minimal-length string that is meaningful, by omitting units that are more significant than those necessary to display dt_sec; if... * dt_sec < 1 s Use engineering formatting for the number. * dt_sec is an integer in the range 0-59 (inclusive) `sec_decimals` is ignored and the number is formatted as an integer See Notes below for handling of units. (Default: False) sec_decimals : int Round seconds to this number of digits Notes ----- If colon notation (e.g. HH:MM:SS.xxx, MM:SS.xxx, etc.) is not used, the number is only seconds, and is appended by a space ' ' followed by units of 's' (possibly with a metric prefix). """ sign_str = '' sgn = 1 if dt_sec < 0: sgn = -1 sign_str = '-' dt_sec = sgn*dt_sec h, r = divmod(dt_sec, 3600) m, s = divmod(r, 60) h = int(h) m = int(m) strdt = '' if hms_always or h != 0: strdt += format(h, '02d') + ':' if hms_always or h != 0 or m != 0: strdt += format(m, '02d') + ':' if float(s) == int(s): s = int(s) s_fmt = 'd' if len(strdt) == 0 else '02d' else: # If no hours or minutes, use SI-prefixed fmt for seconds with 3 # decimal places if (h == 0) and (m == 0) and not hms_always: nearest_si_order_of_mag = ( (decimal.Decimal.from_float(dt_sec).adjusted() // 3) * 3 ) sec_str = format_num(dt_sec, precision=10**(nearest_si_order_of_mag-3), exponent=nearest_si_order_of_mag, fmt='sipre') return sec_str + 's' # Otherwise, round seconds to sec_decimals decimal digits s = np.round(s, sec_decimals) if len(strdt) == 0: s_fmt = '.%df' %sec_decimals else: if sec_decimals == 0: s_fmt = '02.0f' else: s_fmt = '0%d.%df' %(3+sec_decimals, sec_decimals) if len(strdt) > 0: strdt += format(s, s_fmt) else: strdt += format(s, s_fmt) + ' s' return sign_str + strdt
[docs] def test_timediff(): """Unit tests for timediff function""" v = timediff(1234) assert v == '20:34', v v = timediff(1234.5678) assert v == '20:34.568', v v = timediff(1, hms_always=True) assert v == '00:00:01', v v = timediff(1.1, hms_always=True, sec_decimals=3) assert v == '00:00:01.100', v v = timediff(1e6) assert v == '277:46:40', v v = timediff(1e6 + 1.5) assert v == '277:46:41.500', v logging.info('<< PASS : test_timediff >>')
[docs] def timestamp(d=True, t=True, tz=True, utc=False, winsafe=False): """Simple utility to print out a time, date, or time+date stamp for the time at which the function is called. Calling via defaults (explcitly provided here for reference) .. :: timestamp(d=True, t=True, tz=True, utc=False, winsafe=False) should be equivalent to the shell command .. :: date +'%Y-%m-%dT%H:%M:%S%z' Parameters ----------: d : bool Include date (default: True) t : bool Include time (default: True) tz : bool Include timezone offset from UTC (default: True) utc : bool Include UTC time/date (as opposed to local time/date) (default: False) winsafe : bool Omit colons between hours/minutes (default: False) """ if utc: time_tuple = time.gmtime() else: time_tuple = time.localtime() dts = '' if d: dts += time.strftime('%Y-%m-%d', time_tuple) if t: dts += 'T' if t: if winsafe: dts += time.strftime('%H%M%S', time_tuple) else: dts += time.strftime('%H:%M:%S', time_tuple) if tz: if utc: if winsafe: dts += time.strftime('+0000') else: dts += time.strftime('+0000') else: offset = time.strftime('%z') if not winsafe: offset = offset[:-2:] + '' + offset[-2::] dts += offset return dts
[docs] def test_timestamp(): """Unit tests for timestamp function""" date_cmd = "date +'%Y-%m-%dT%H:%M:%S%z'" # In case we call these on either side of a second, repeat a few times if not equal for _ in range(10): ref = os.popen(date_cmd).read().strip() test = timestamp(winsafe=False) if test == ref: break time.sleep(0.05) assert test == ref, f'{date_cmd} = "{ref}" but timestamp = "{test}"' logging.info('<< PASS : test_timestamp >>')
if __name__ == '__main__': set_verbosity(Levels.INFO) test_hr_range_formatter() test_list2hrlist() test_format_num() test_timediff() test_timestamp()