Skip to content

Commit 0b789eb

Browse files
committed
Merge pull request #9352 from sinhrks/string_fillchar
ENH: StringMethods now supports ljust and rjust
2 parents ee3a1f1 + 0b679b4 commit 0b789eb

File tree

5 files changed

+149
-30
lines changed

5 files changed

+149
-30
lines changed

doc/source/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,12 +535,14 @@ strings and apply several methods to it. These can be acccessed like
535535
Series.str.get
536536
Series.str.join
537537
Series.str.len
538+
Series.str.ljust
538539
Series.str.lower
539540
Series.str.lstrip
540541
Series.str.match
541542
Series.str.pad
542543
Series.str.repeat
543544
Series.str.replace
545+
Series.str.rjust
544546
Series.str.rstrip
545547
Series.str.slice
546548
Series.str.slice_replace

doc/source/text.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,9 @@ Method Summary
212212
:meth:`~Series.str.replace`,Replace occurrences of pattern/regex with some other string
213213
:meth:`~Series.str.repeat`,Duplicate values (``s.str.repeat(3)`` equivalent to ``x * 3``)
214214
:meth:`~Series.str.pad`,"Add whitespace to left, right, or both sides of strings"
215-
:meth:`~Series.str.center`,Equivalent to ``pad(side='both')``
215+
:meth:`~Series.str.center`,Equivalent to ``str.center``
216+
:meth:`~Series.str.ljust`,Equivalent to ``str.ljust``
217+
:meth:`~Series.str.rjust`,Equivalent to ``str.rjust``
216218
:meth:`~Series.str.wrap`,Split long strings into lines with length less than a given width
217219
:meth:`~Series.str.slice`,Slice each string in the Series
218220
:meth:`~Series.str.slice_replace`,Replace slice in each string with passed value

doc/source/whatsnew/v0.16.0.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ Enhancements
111111
- Added ``StringMethods.isalnum()``, ``isalpha()``, ``isdigit()``, ``isspace()``, ``islower()``,
112112
``isupper()``, ``istitle()`` which behave as the same as standard ``str`` (:issue:`9282`)
113113

114+
115+
116+
- Added ``StringMethods.ljust()`` and ``rjust()`` which behave as the same as standard ``str`` (:issue:`9352`)
117+
- ``StringMethods.pad()`` and ``center()`` now accept `fillchar` option to specify filling character (:issue:`9352`)
118+
114119
Performance
115120
~~~~~~~~~~~
116121

pandas/core/strings.py

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pandas.compat import zip
44
from pandas.core.common import isnull, _values_from_object
55
import pandas.compat as compat
6+
from pandas.util.decorators import Appender
67
import re
78
import pandas.lib as lib
89
import warnings
@@ -543,9 +544,9 @@ def str_findall(arr, pat, flags=0):
543544
return _na_map(regex.findall, arr)
544545

545546

546-
def str_pad(arr, width, side='left'):
547+
def str_pad(arr, width, side='left', fillchar=' '):
547548
"""
548-
Pad strings with whitespace
549+
Pad strings with an additional character
549550
550551
Parameters
551552
----------
@@ -554,40 +555,33 @@ def str_pad(arr, width, side='left'):
554555
Minimum width of resulting string; additional characters will be filled
555556
with spaces
556557
side : {'left', 'right', 'both'}, default 'left'
558+
fillchar : str
559+
Additional character for filling, default is whitespace
557560
558561
Returns
559562
-------
560563
padded : array
561564
"""
565+
566+
if not isinstance(fillchar, compat.string_types):
567+
msg = 'fillchar must be a character, not {0}'
568+
raise TypeError(msg.format(type(fillchar).__name__))
569+
570+
if len(fillchar) != 1:
571+
raise TypeError('fillchar must be a character, not str')
572+
562573
if side == 'left':
563-
f = lambda x: x.rjust(width)
574+
f = lambda x: x.rjust(width, fillchar)
564575
elif side == 'right':
565-
f = lambda x: x.ljust(width)
576+
f = lambda x: x.ljust(width, fillchar)
566577
elif side == 'both':
567-
f = lambda x: x.center(width)
578+
f = lambda x: x.center(width, fillchar)
568579
else: # pragma: no cover
569580
raise ValueError('Invalid side')
570581

571582
return _na_map(f, arr)
572583

573584

574-
def str_center(arr, width):
575-
"""
576-
"Center" strings, filling left and right side with additional whitespace
577-
578-
Parameters
579-
----------
580-
width : int
581-
Minimum width of resulting string; additional characters will be filled
582-
with spaces
583-
584-
Returns
585-
-------
586-
centered : array
587-
"""
588-
return str_pad(arr, width, side='both')
589-
590-
591585
def str_split(arr, pat=None, n=None, return_type='series'):
592586
"""
593587
Split each string (a la re.split) in array by given pattern, propagating NA
@@ -978,14 +972,37 @@ def repeat(self, repeats):
978972
return self._wrap_result(result)
979973

980974
@copy(str_pad)
981-
def pad(self, width, side='left'):
982-
result = str_pad(self.series, width, side=side)
975+
def pad(self, width, side='left', fillchar=' '):
976+
result = str_pad(self.series, width, side=side, fillchar=fillchar)
983977
return self._wrap_result(result)
984978

985-
@copy(str_center)
986-
def center(self, width):
987-
result = str_center(self.series, width)
988-
return self._wrap_result(result)
979+
_shared_docs['str_pad'] = ("""
980+
"Center" strings, filling %s side with an additional character
981+
982+
Parameters
983+
----------
984+
width : int
985+
Minimum width of resulting string; additional characters will be filled
986+
with ``fillchar``
987+
fillchar : str
988+
Additional character for filling, default is whitespace
989+
990+
Returns
991+
-------
992+
centered : array
993+
""")
994+
995+
@Appender(_shared_docs['str_pad'] % 'left and right')
996+
def center(self, width, fillchar=' '):
997+
return self.pad(width, side='both', fillchar=fillchar)
998+
999+
@Appender(_shared_docs['str_pad'] % 'right')
1000+
def ljust(self, width, fillchar=' '):
1001+
return self.pad(width, side='right', fillchar=fillchar)
1002+
1003+
@Appender(_shared_docs['str_pad'] % 'left')
1004+
def rjust(self, width, fillchar=' '):
1005+
return self.pad(width, side='left', fillchar=fillchar)
9891006

9901007
@copy(str_slice)
9911008
def slice(self, start=None, stop=None, step=None):

pandas/tests/test_strings.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,21 +805,62 @@ def test_pad(self):
805805
u('eeeeee')])
806806
tm.assert_almost_equal(result, exp)
807807

808-
def test_center(self):
808+
def test_pad_fillchar(self):
809+
810+
values = Series(['a', 'b', NA, 'c', NA, 'eeeeee'])
811+
812+
result = values.str.pad(5, side='left', fillchar='X')
813+
exp = Series(['XXXXa', 'XXXXb', NA, 'XXXXc', NA, 'eeeeee'])
814+
tm.assert_almost_equal(result, exp)
815+
816+
result = values.str.pad(5, side='right', fillchar='X')
817+
exp = Series(['aXXXX', 'bXXXX', NA, 'cXXXX', NA, 'eeeeee'])
818+
tm.assert_almost_equal(result, exp)
819+
820+
result = values.str.pad(5, side='both', fillchar='X')
821+
exp = Series(['XXaXX', 'XXbXX', NA, 'XXcXX', NA, 'eeeeee'])
822+
tm.assert_almost_equal(result, exp)
823+
824+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not str"):
825+
result = values.str.pad(5, fillchar='XY')
826+
827+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not int"):
828+
result = values.str.pad(5, fillchar=5)
829+
830+
def test_center_ljust_rjust(self):
809831
values = Series(['a', 'b', NA, 'c', NA, 'eeeeee'])
810832

811833
result = values.str.center(5)
812834
exp = Series([' a ', ' b ', NA, ' c ', NA, 'eeeeee'])
813835
tm.assert_almost_equal(result, exp)
814836

837+
result = values.str.ljust(5)
838+
exp = Series(['a ', 'b ', NA, 'c ', NA, 'eeeeee'])
839+
tm.assert_almost_equal(result, exp)
840+
841+
result = values.str.rjust(5)
842+
exp = Series([' a', ' b', NA, ' c', NA, 'eeeeee'])
843+
tm.assert_almost_equal(result, exp)
844+
815845
# mixed
816846
mixed = Series(['a', NA, 'b', True, datetime.today(),
817847
'c', 'eee', None, 1, 2.])
818848

819849
rs = Series(mixed).str.center(5)
820850
xp = Series([' a ', NA, ' b ', NA, NA, ' c ', ' eee ', NA, NA,
821851
NA])
852+
tm.assert_isinstance(rs, Series)
853+
tm.assert_almost_equal(rs, xp)
822854

855+
rs = Series(mixed).str.ljust(5)
856+
xp = Series(['a ', NA, 'b ', NA, NA, 'c ', 'eee ', NA, NA,
857+
NA])
858+
tm.assert_isinstance(rs, Series)
859+
tm.assert_almost_equal(rs, xp)
860+
861+
rs = Series(mixed).str.rjust(5)
862+
xp = Series([' a', NA, ' b', NA, NA, ' c', ' eee', NA, NA,
863+
NA])
823864
tm.assert_isinstance(rs, Series)
824865
tm.assert_almost_equal(rs, xp)
825866

@@ -832,6 +873,58 @@ def test_center(self):
832873
u('eeeeee')])
833874
tm.assert_almost_equal(result, exp)
834875

876+
result = values.str.ljust(5)
877+
exp = Series([u('a '), u('b '), NA, u('c '), NA,
878+
u('eeeeee')])
879+
tm.assert_almost_equal(result, exp)
880+
881+
result = values.str.rjust(5)
882+
exp = Series([u(' a'), u(' b'), NA, u(' c'), NA,
883+
u('eeeeee')])
884+
tm.assert_almost_equal(result, exp)
885+
886+
def test_center_ljust_rjust_fillchar(self):
887+
values = Series(['a', 'bb', 'cccc', 'ddddd', 'eeeeee'])
888+
889+
result = values.str.center(5, fillchar='X')
890+
expected = Series(['XXaXX', 'XXbbX', 'Xcccc', 'ddddd', 'eeeeee'])
891+
tm.assert_series_equal(result, expected)
892+
expected = np.array([v.center(5, 'X') for v in values.values])
893+
tm.assert_numpy_array_equal(result.values, expected)
894+
895+
result = values.str.ljust(5, fillchar='X')
896+
expected = Series(['aXXXX', 'bbXXX', 'ccccX', 'ddddd', 'eeeeee'])
897+
tm.assert_series_equal(result, expected)
898+
expected = np.array([v.ljust(5, 'X') for v in values.values])
899+
tm.assert_numpy_array_equal(result.values, expected)
900+
901+
result = values.str.rjust(5, fillchar='X')
902+
expected = Series(['XXXXa', 'XXXbb', 'Xcccc', 'ddddd', 'eeeeee'])
903+
tm.assert_series_equal(result, expected)
904+
expected = np.array([v.rjust(5, 'X') for v in values.values])
905+
tm.assert_numpy_array_equal(result.values, expected)
906+
907+
# If fillchar is not a charatter, normal str raises TypeError
908+
# 'aaa'.ljust(5, 'XY')
909+
# TypeError: must be char, not str
910+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not str"):
911+
result = values.str.center(5, fillchar='XY')
912+
913+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not str"):
914+
result = values.str.ljust(5, fillchar='XY')
915+
916+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not str"):
917+
result = values.str.rjust(5, fillchar='XY')
918+
919+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not int"):
920+
result = values.str.center(5, fillchar=1)
921+
922+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not int"):
923+
result = values.str.ljust(5, fillchar=1)
924+
925+
with tm.assertRaisesRegexp(TypeError, "fillchar must be a character, not int"):
926+
result = values.str.rjust(5, fillchar=1)
927+
835928
def test_split(self):
836929
values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h'])
837930

0 commit comments

Comments
 (0)