import numpy as np
import pandas as pd
from datetime import (
date,
datetime,
time,
)
from fireant.slicer.totals import TOTALS_MARKERS
INF_VALUE = "Inf"
NULL_VALUE = 'null'
NAN_VALUE = 'NaN'
TOTALS_VALUE = 'totals'
RAW_VALUE = 'raw'
NO_TIME = time(0)
epoch = np.datetime64(datetime.utcfromtimestamp(0))
milliseconds = np.timedelta64(1, 'ms')
[docs]def date_as_millis(value):
if not isinstance(value, date):
value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
return int(1000 * value.timestamp())
[docs]def coerce_type(value):
if value is None:
return None
if isinstance(value, date):
return value
# Should never be any real NaNs or INFs at this point, so if that's the value, it's meant to be that.
if isinstance(value, str) and str.lower(value) in ['nan', 'inf']:
return value
for type_cast in (int, float):
try:
return type_cast(value)
except:
pass
if NULL_VALUE == value:
return None
if 'True' == value:
return True
if 'False' == value:
return False
return value
[docs]def dimension_value(value):
"""
Format a dimension value. This will coerce the raw string or date values into a proper primitive value like a
string, float, or int.
:param value:
The raw str or datetime value
:param str_date:
When True, dates and datetimes will be converted to ISO strings. The time is omitted for dates. When False, the
datetime will be converted to a POSIX timestamp (millis-since-epoch).
"""
if value in TOTALS_MARKERS:
return 'Totals'
if pd.isnull(value):
return NULL_VALUE
if isinstance(value, date):
if not hasattr(value, 'time') or value.time() == NO_TIME:
return value.strftime('%Y-%m-%d')
else:
return value.strftime('%Y-%m-%d %H:%M:%S')
return coerce_type(value)
[docs]def metric_value(value):
"""
Converts a raw metric value into a safe type. This will change dates into strings, NaNs into Nones, and np types
into their corresponding python types.
:param value:
The raw metric value.
"""
if isinstance(value, date):
if not hasattr(value, 'time') or value.time() == NO_TIME:
return value.strftime('%Y-%m-%d')
else:
return value.strftime('%Y-%m-%dT%H:%M:%S')
if value is None or value is '' or pd.isnull(value):
return None
if isinstance(value, float):
if np.isinf(value):
return None
return float(value)
if isinstance(value, np.int64):
# Cannot transform np.int64 to json
return int(value)
return value
[docs]def metric_display(value, prefix=None, suffix=None, precision=None):
"""
Converts a metric value into the display value by applying formatting.
:param value:
The raw metric value.
:param prefix:
An optional prefix.
:param suffix:
An optional suffix.
:param precision:
The decimal precision, the number of decimal places to round to.
:return:
A formatted string containing the display value for the metric.
"""
if pd.isnull(value):
value = NULL_VALUE
if value in {np.inf, -np.inf, INF_VALUE}:
return INF_VALUE
if value in (NULL_VALUE, NAN_VALUE):
return NULL_VALUE
if isinstance(value, bool):
value = str(value).lower()
if prefix == '$' and isinstance(value, (float, int)) and value < 0:
value = -value
prefix = '-$'
if isinstance(value, float):
if precision is not None:
value = '{:,.{precision}f}'.format(value, precision=precision)
elif value.is_integer():
value = '{:,.0f}'.format(value)
else:
# Stripping trailing zeros is necessary because %f can add them if no precision is set
value = '{:,f}'.format(value).rstrip('.0')
if isinstance(value, int):
value = '{:,.0f}'.format(value)
return '{prefix}{value}{suffix}'.format(
prefix=prefix or '',
value=str(value),
suffix=suffix or '',
)