diff --git a/pandas/core/format.py b/pandas/core/format.py index ae0d95b1c3074..6ba7bfcc8f16c 100644 --- a/pandas/core/format.py +++ b/pandas/core/format.py @@ -62,6 +62,7 @@ ------- formatted : string (or unicode, depending on data and options)""" + class CategoricalFormatter(object): def __init__(self, categorical, buf=None, length=True, na_rep='NaN', name=False, footer=True): @@ -140,7 +141,7 @@ def __init__(self, series, buf=None, header=True, length=True, if float_format is None: float_format = get_option("display.float_format") self.float_format = float_format - self.dtype = dtype + self.dtype = dtype def _get_footer(self): footer = u('') @@ -163,7 +164,7 @@ def _get_footer(self): footer += 'Length: %d' % len(self.series) if self.dtype: - if getattr(self.series.dtype,'name',None): + if getattr(self.series.dtype, 'name', None): if footer: footer += ', ' footer += 'dtype: %s' % com.pprint_thing(self.series.dtype.name) @@ -213,6 +214,7 @@ def to_string(self): return compat.text_type(u('\n').join(result)) + def _strlen_func(): if compat.PY3: # pragma: no cover _strlen = len @@ -304,32 +306,31 @@ def _to_str_columns(self): for i, c in enumerate(self.columns): if self.header: - fmt_values = self._format_col(i) cheader = str_columns[i] - max_colwidth = max(self.col_space or 0, *(_strlen(x) for x in cheader)) - - fmt_values = _make_fixed_width(fmt_values, self.justify, - minimum=max_colwidth) + fmt_values = self._format_col(i, justify=self.justify, + minimum=max_colwidth) max_len = max(np.max([_strlen(x) for x in fmt_values]), max_colwidth) if self.justify == 'left': cheader = [x.ljust(max_len) for x in cheader] - else: + elif self.justify == 'right': cheader = [x.rjust(max_len) for x in cheader] + elif self.justify == 'center': + cheader = [x.center(max_len) for x in cheader] + else: + cheader = [x.strip() for x in cheader] stringified.append(cheader + fmt_values) else: - stringified = [_make_fixed_width(self._format_col(i), - self.justify) + stringified = [self._format_col(i, justify=self.justify) for i, c in enumerate(self.columns)] strcols = stringified if self.index: strcols.insert(0, str_index) - return strcols def to_string(self, force_unicode=None): @@ -450,12 +451,14 @@ def write(buf, frame, column_format, strcols): raise TypeError('buf is not a file name and it has no write ' 'method') - def _format_col(self, i): + def _format_col(self, i, justify='right', minimum=None): formatter = self._get_formatter(i) return format_array(self.frame.icol(i).get_values(), formatter, float_format=self.float_format, na_rep=self.na_rep, - space=self.col_space) + space=self.col_space, + justify=justify, + minimum=minimum) def to_html(self, classes=None): """ @@ -735,7 +738,7 @@ def _write_body(self, indent): fmt_values = {} for i in range(len(self.columns)): - fmt_values[i] = self.fmt._format_col(i) + fmt_values[i] = self.fmt._format_col(i, justify=None) # write values if self.fmt.index: @@ -1485,7 +1488,7 @@ def get_formatted_cells(self): def format_array(values, formatter, float_format=None, na_rep='NaN', - digits=None, space=None, justify='right'): + digits=None, space=None, justify='right', minimum=None): if com.is_float_dtype(values.dtype): fmt_klass = FloatArrayFormatter elif com.is_integer_dtype(values.dtype): @@ -1509,7 +1512,7 @@ def format_array(values, formatter, float_format=None, na_rep='NaN', fmt_obj = fmt_klass(values, digits, na_rep=na_rep, float_format=float_format, formatter=formatter, space=space, - justify=justify) + justify=justify, minimum=minimum) return fmt_obj.get_result() @@ -1517,7 +1520,7 @@ def format_array(values, formatter, float_format=None, na_rep='NaN', class GenericArrayFormatter(object): def __init__(self, values, digits=7, formatter=None, na_rep='NaN', - space=12, float_format=None, justify='right'): + space=12, float_format=None, justify='right', minimum=None): self.values = values self.digits = digits self.na_rep = na_rep @@ -1525,10 +1528,11 @@ def __init__(self, values, digits=7, formatter=None, na_rep='NaN', self.formatter = formatter self.float_format = float_format self.justify = justify + self.minimum = minimum def get_result(self): fmt_values = self._format_strings() - return _make_fixed_width(fmt_values, self.justify) + return _make_fixed_width(fmt_values, self.justify, self.minimum) def _format_strings(self): if self.float_format is None: @@ -1584,19 +1588,19 @@ def __init__(self, *args, **kwargs): def _format_with(self, fmt_str): def _val(x, threshold): if notnull(x): - if threshold is None or abs(x) > get_option("display.chop_threshold"): + if threshold is None or abs(x) > get_option("display.chop_threshold"): return fmt_str % x else: - if fmt_str.endswith("e"): # engineering format - return "0" + if fmt_str.endswith("e"): # engineering format + return "0" else: - return fmt_str % 0 + return fmt_str % 0 else: return self.na_rep threshold = get_option("display.chop_threshold") - fmt_values = [ _val(x, threshold) for x in self.values] + fmt_values = [_val(x, threshold) for x in self.values] return _trim_zeros(fmt_values, self.na_rep) def get_result(self): @@ -1627,7 +1631,7 @@ def get_result(self): fmt_str = '%% .%de' % (self.digits - 1) fmt_values = self._format_with(fmt_str) - return _make_fixed_width(fmt_values, self.justify) + return _make_fixed_width(fmt_values, self.justify, self.minimum) class IntArrayFormatter(GenericArrayFormatter): @@ -1640,7 +1644,7 @@ def get_result(self): fmt_values = [formatter(x) for x in self.values] - return _make_fixed_width(fmt_values, self.justify) + return _make_fixed_width(fmt_values, self.justify, self.minimum) class Datetime64Formatter(GenericArrayFormatter): @@ -1652,7 +1656,8 @@ def get_result(self): formatter = _format_datetime64 fmt_values = [formatter(x) for x in self.values] - return _make_fixed_width(fmt_values, self.justify) + return _make_fixed_width(fmt_values, self.justify, self.minimum) + def _format_datetime64(x, tz=None): if isnull(x): @@ -1672,7 +1677,8 @@ def get_result(self): formatter = _format_timedelta64 fmt_values = [formatter(x) for x in self.values] - return _make_fixed_width(fmt_values, self.justify) + return _make_fixed_width(fmt_values, self.justify, self.minimum) + def _format_timedelta64(x): if isnull(x): @@ -1680,6 +1686,7 @@ def _format_timedelta64(x): return lib.repr_timedelta64(x) + def _make_fixed_width(strings, justify='right', minimum=None): if len(strings) == 0: return strings @@ -1697,8 +1704,12 @@ def _make_fixed_width(strings, justify='right', minimum=None): if justify == 'left': justfunc = lambda self, x: self.ljust(x) - else: + elif justify == 'right': justfunc = lambda self, x: self.rjust(x) + elif justify == 'center': + justfunc = lambda self, x: self.center(x) + else: + justfunc = lambda self, _: self.strip() def just(x): eff_len = max_len diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py index d9bf8adb71298..f3b09b227cc10 100644 --- a/pandas/tests/test_format.py +++ b/pandas/tests/test_format.py @@ -399,13 +399,13 @@ def test_to_html_escaped(self): str<ing1 &amp; - <type 'str'> - <type 'str'> + <type 'str'> + <type 'str'> stri>ng2 &amp; - <type 'str'> - <type 'str'> + <type 'str'> + <type 'str'> """ @@ -431,13 +431,13 @@ def test_to_html_escape_disabled(self): str - bold - bold + bold + bold stri>ng2 & - bold - bold + bold + bold """ @@ -471,26 +471,26 @@ def test_to_html_multiindex_sparsify_false_multi_sparse(self): 0 0 - 0 - 1 + 0 + 1 0 1 - 2 - 3 + 2 + 3 1 0 - 4 - 5 + 4 + 5 1 1 - 6 - 7 + 6 + 7 """ @@ -526,26 +526,26 @@ def test_to_html_multiindex_sparsify_false_multi_sparse(self): 0 0 - 0 - 1 + 0 + 1 0 1 - 2 - 3 + 2 + 3 1 0 - 4 - 5 + 4 + 5 1 1 - 6 - 7 + 6 + 7 """ @@ -577,24 +577,24 @@ def test_to_html_multiindex_sparsify(self): 0 0 - 0 - 1 + 0 + 1 1 - 2 - 3 + 2 + 3 1 0 - 4 - 5 + 4 + 5 1 - 6 - 7 + 6 + 7 """ @@ -630,24 +630,24 @@ def test_to_html_multiindex_sparsify(self): 0 0 - 0 - 1 + 0 + 1 1 - 2 - 3 + 2 + 3 1 0 - 4 - 5 + 4 + 5 1 - 6 - 7 + 6 + 7 """ @@ -671,23 +671,23 @@ def test_to_html_index_formatter(self): a - 0 - 1 + 0 + 1 b - 2 - 3 + 2 + 3 c - 4 - 5 + 4 + 5 d - 6 - 7 + 6 + 7 """ @@ -1159,7 +1159,7 @@ def test_to_string_left_justify_cols(self): df_s = df.to_string(justify='left') expected = (' x \n' '0 3234.000\n' - '1 0.253') + '1 0.253 ') assert(df_s == expected) def test_to_string_format_na(self): @@ -1274,17 +1274,17 @@ def test_to_html_multiindex(self): ' \n' ' \n' ' 0\n' - ' a\n' - ' b\n' - ' c\n' - ' d\n' + ' a\n' + ' b\n' + ' c\n' + ' d\n' ' \n' ' \n' ' 1\n' - ' e\n' - ' f\n' - ' g\n' - ' h\n' + ' e\n' + ' f\n' + ' g\n' + ' h\n' ' \n' ' \n' '') @@ -1316,17 +1316,17 @@ def test_to_html_multiindex(self): ' \n' ' \n' ' 0\n' - ' a\n' - ' b\n' - ' c\n' - ' d\n' + ' a\n' + ' b\n' + ' c\n' + ' d\n' ' \n' ' \n' ' 1\n' - ' e\n' - ' f\n' - ' g\n' - ' h\n' + ' e\n' + ' f\n' + ' g\n' + ' h\n' ' \n' ' \n' '') @@ -1351,21 +1351,21 @@ def test_to_html_justify(self): ' \n' ' \n' ' 0\n' - ' 6\n' - ' 1\n' - ' 223442\n' + ' 6\n' + ' 1\n' + ' 223442\n' ' \n' ' \n' ' 1\n' - ' 30000\n' - ' 2\n' - ' 0\n' + ' 30000\n' + ' 2\n' + ' 0\n' ' \n' ' \n' ' 2\n' - ' 2\n' - ' 70000\n' - ' 1\n' + ' 2\n' + ' 70000\n' + ' 1\n' ' \n' ' \n' '') @@ -1385,21 +1385,21 @@ def test_to_html_justify(self): ' \n' ' \n' ' 0\n' - ' 6\n' - ' 1\n' - ' 223442\n' + ' 6\n' + ' 1\n' + ' 223442\n' ' \n' ' \n' ' 1\n' - ' 30000\n' - ' 2\n' - ' 0\n' + ' 30000\n' + ' 2\n' + ' 0\n' ' \n' ' \n' ' 2\n' - ' 2\n' - ' 70000\n' - ' 1\n' + ' 2\n' + ' 70000\n' + ' 1\n' ' \n' ' \n' '') @@ -1934,6 +1934,69 @@ def test_misc(self): obj = fmt.FloatArrayFormatter(np.array([], dtype=np.float64)) result = obj.get_result() + +class TestDataFrameJustification(unittest.TestCase): + def setUp(self): + self.df_int = pd.DataFrame(np.arange(3).reshape(1, 3), + columns=['foo', 'bar', 'baz'], + dtype=int) + self.df_float = pd.DataFrame(np.linspace(0.3, 1.2, 3).reshape(1, 3), + columns=['a', 'b', 'c'], + dtype=float) + self.df_string = pd.DataFrame([ + ['test', 'something long', 'something even longer'], + ['foo', 'bar', 'baz'], + ['small', 'text', 'samples'] + ], columns=['a', 'b', 'c']) + + def test_left_justification(self): + expected = ' foo bar baz\n0 0 1 2 ' + self.assertEqual(expected, self.df_int.to_string(justify='left'), + 'Left int justification failed') + expected = ' a b c \n0 0.3 0.75 1.2' + self.assertEqual(expected, self.df_float.to_string(justify='left'), + 'Left float justification failed') + expected = ''' + a b c +0 test something long something even longer +1 foo bar baz +2 small text samples +'''.strip('\r\n') + self.assertEqual(expected, self.df_string.to_string(justify='left'), + 'Left string justification failed') + + def test_right_justification(self): + expected = ' foo bar baz\n0 0 1 2' + self.assertEqual(expected, self.df_int.to_string(justify='right'), + 'Right int justification failed') + expected = ' a b c\n0 0.3 0.75 1.2' + self.assertEqual(expected, self.df_float.to_string(justify='right'), + 'Right float justification failed') + expected = ''' + a b c +0 test something long something even longer +1 foo bar baz +2 small text samples +'''.strip('\r\n') + self.assertEqual(expected, self.df_string.to_string(justify='right'), + 'Right string justification failed') + + def test_center_justification(self): + expected = ' foo bar baz\n0 0 1 2 ' + self.assertEqual(expected, self.df_int.to_string(justify='center'), + 'Center int justification failed') + expected = ' a b c \n0 0.3 0.75 1.2' + self.assertEqual(expected, self.df_float.to_string(justify='center'), + 'Center float justification failed') + expected = ''' + a b c +0 test something long something even longer +1 foo bar baz +2 small text samples +'''.strip('\r\n') + self.assertEqual(expected, self.df_string.to_string(justify='center'), + 'Center string justification failed') + if __name__ == '__main__': import nose nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],