user1934887
user1934887

Reputation: 297

Slicing strings in str.format

I want to achieve the following with str.format:

x,y = 1234,5678
print str(x)[2:] + str(y)[:2]

The only way I was able to do it was:

print '{0}{1}'.format(str(x)[2:],str(y)[:2])

Now, this an example and what I really have is a long and messy string, and so I want to put slicing inside the {}. I've studied the docs, but I can't figure out the correct syntax. My question is: is it possible to slice strings inside a replacement field?

Upvotes: 26

Views: 14878

Answers (7)

Zhang Fan
Zhang Fan

Reputation: 83

I tried to save a group of f-strings like new_template = '{old[0]} is {old[2:]}', hoping to use new = new_template.format(old=old), it doesn't work.

If you are in a similar situation, you can just use a group of lambdas, new_maker = lambda old: f'{old[0]} is {old[2:]}', and use it like new = new_maker(old)

Upvotes: 0

Shiva Gupta
Shiva Gupta

Reputation: 27

I tried doing it in python 3.9 and it is working well

 x="nowpossible"
print(" slicing is possible {}".format(x[0:2]))

output

slicing is possible now

Upvotes: -1

Martijn Pieters
Martijn Pieters

Reputation: 1125338

No, you can't apply slicing to strings inside a the replacement field.

You'll need to refer to the Format Specification Mini-Language; it defines what is possible. This mini language defines how you format the referenced value (the part after the : in the replacement field syntax).

Upvotes: 19

Nicolas Ryberg
Nicolas Ryberg

Reputation: 46

Straight answering your question: No, slicing is not supported by builtin str formatting. Although, there is a workaround in case f-strings (runtime evaluated) don't fit your needs.

Workaround

The previous answers to extend string.Formatter are not completely right, since overloading get_value is not the correct way to add the slicing mechanism to string.Formatter.

import string


def transform_to_slice_index(val: str):
    if val == "_":
        return None
    else:
        return int(val)


class SliceFormatter(string.Formatter):

    def get_field(self, field_name, args, kwargs):
        slice_operator = None
        if type(field_name) == str and '|' in field_name:
            field_name, slice_indexes = field_name.split('|')
            slice_indexes = map(transform_to_slice_index,
                                slice_indexes.split(','))
            slice_operator = slice(*slice_indexes)

        obj, first = super().get_field(field_name, args, kwargs)
        if slice_operator is not None:
            obj = obj[slice_operator]

        return obj, first

Explanation

get_value is called inside get_field and it is used ONLY to access the args and kwargs from vformat(). attr and item accessing is done in get_field. Thus, the slice access should be done after super().get_field returned the desired obj.

With this said, overloading get_value gives you the problem that the formatter would not work for slicing after the object is traversed. You can see the error in this example:

WrongSliceFormatter().format("{foo.bar[0]|1,3}", foo=foo)
>> ValueError: "Only '.' or '[' may follow ']' in format field specifier"

Upvotes: 2

Erik Aronesty
Erik Aronesty

Reputation: 12945

You can use a run-time evaluated "f" string. Python f-strings support slicing and don't use a "mini-language" like the formatter. The full power of a python expression is available within each curly-brace of an f-string. Unfortunately there is no string.feval() function ... imo there should be (languages should not have magic abilities that are not provided to the user).

You also can't add one to the string type, because the built-in python types cannot be modified/expanded.

See https://stackoverflow.com/a/49884004/627042 for an example of a run-time evaluates f-string.

Upvotes: 2

Michael Marchionna
Michael Marchionna

Reputation: 11

This is a nice solution and solved my slicing problem quite nicely. However, I also wanted to do value eliding as well. For example 'AVeryLongStringValue' that I might want to stuff in a 10 character field, might be truncated to '...ngValue'. So I extended your example to support slicing, eliding, and normal formatting all in one. This is what I came up with.

class SliceElideFormatter(string.Formatter):
    """An extended string formatter that provides key specifiers that allow
    string values to be sliced and elided if they exceed a length limit.  The
    additional formats are optional and can be combined with normal python
    formatting.  So the whole syntax looks like:
    key[|slice-options][$elide-options[:normal-options]
    Where slice options consist of '|' character to begin a slice request,
    followed by slice indexes separated by commas.  Thus {FOO|5,} requests
    everything after the 5th element.
      The elide consist of '$' character followed by an inter max field value,
    followed by '<', '^', or '>' for pre, centered, or post eliding, followed
    by the eliding string.  Thus {FOO$10<-} would display the last 9 chanacters
    of a string longer then 10 characters with '-' prefix.
      Slicing and eliding can be combined.  For example given a dict of
    {'FOO': 'centeredtextvalue', and a format string of 
    '{FOO|1,-1$11^%2E%2E%2E}' would yield 'ente...valu'.  The slice spec removes
    the first and last characrers, and the elide spec center elides the
    remaining value with '...'.  The '...' value must be encoded in URL format
    since . is an existing special format character.
    """

    def get_value(self, key, args, kwds):
        """Called by string.Formatter for each format key found in the format
        string.  The key is checked for the presence of a slice or elide intro-
        ducer character.  If one or both a found the slice and/or elide spec
        is extracted, parsed and processed on value of found with the remaining
        key string.
        Arguments:
          key, A format key string possibly containing slice or elide specs
          args, Format values list tuple
          kwds, Format values key word dictrionary
        """
        sspec = espec = None
        if '|' in key:
            key, sspec = key.split('|')
            if '$' in sspec:
                sspec, espec = sspec.split('$')
        elif '$' in key:
            key, espec = key.split('$')
        value = args[int(key)] if key.isdigit() else kwds[key]
        if sspec:
            sindices = [int(sdx) if sdx else None
                        for sdx in sspec.split(',')]
            value = value[slice(*sindices)]
        if espec:
            espec = urllib.unquote(espec)
            if '<' in espec:
                value = self._prefix_elide_value(espec, value)
            elif '>' in espec:
                value = self._postfix_elide_value(espec, value)
            elif '^' in espec:
                value = self._center_elide_value(espec, value)
            else:
                raise ValueError('invalid eliding option %r' % elidespec)
        if sspec or espec:
            return value

        return super(SliceElideFormatter,self).get_value(key, args, kwds)

    def _center_elide_value(self, elidespec, value):
        """Return center elide value if it exceeds the elide length.
        Arguments:
          elidespec, The elide spec field extracted from key
          value, Value obtained from remaing key to maybe be elided
        """
        elidelen, elidetxt = elidespec.split('^')
        elen, vlen = int(elidelen), len(value)
        if vlen > elen:
            tlen = len(elidetxt)
            return value[:(elen-tlen)//2] + elidetxt + value[-(elen-tlen)//2:]
        return value

    def _postfix_elide_value(self, elidespec, value):
        """Return postfix elided value if it exceeds the elide length.
        Arguments:
          elidespec, The elide spec field extracted from key
          value, Value obtained from remaing key to maybe be elided
        """
        elidelen, elidetxt = elidespec.split('>')
        elen, vlen  = int(elidelen), len(value)
        if vlen > elen:
            tlen = len(elidetxt)
            return value[:(elen-tlen)] + elidetxt
        return value

    def _prefix_elide_value(self, elidespec, value):
        """Return prefix elided value if it exceeds the elide length.
        Arguments:
          elidespec, The elide spec field extracted from key
          value, Value obtained from remaing key to maybe be elided
        """
        elidelen, elidetxt = elidespec.split('<')
        elen, vlen  = int(elidelen), len(value)
        if vlen > elen:
            tlen = len(elidetxt)
            return elidetxt + value[-(elen-tlen):]
        return value

As an example all three format specs can be combined to clip the values first and last characters, center elide the slice to a 10 char value, and finally right justify it in a 12 char field as follows:

sefmtr = SliceElideFormatter()
data = { 'CNT':'centeredtextvalue' }
fmt = '{CNT|1,-1$10^**:>12}'
print '%r' % sefmtr.format(fmt, *(), **data)

Outputs: ' ente**valu'. For anyone else that may be interested. Thanks much.

Upvotes: 1

sberry
sberry

Reputation: 132138

You could do something like this.

NOTE
This is a rough example and should not be considered complete and tested. But I think it shows you a way to start getting where you want to be.

import string

class SliceFormatter(string.Formatter):

    def get_value(self, key, args, kwds):
        if '|' in key:
            try:
                key, indexes = key.split('|')
                indexes = map(int, indexes.split(','))
                if key.isdigit():
                    return args[int(key)][slice(*indexes)]
                return kwds[key][slice(*indexes)]
            except KeyError:
                return kwds.get(key, 'Missing')
        return super(SliceFormatter, self).get_value(key, args, kwds)


phrase = "Hello {name|0,5}, nice to meet you.  I am {name|6,9}.  That is {0|0,4}."
fmt = SliceFormatter()
print fmt.format(phrase, "JeffJeffJeff", name="Larry Bob")

OUTPUT

Hello Larry, nice to meet you.  I am Bob.  That is Jeff.

NOTE 2
There is no support for slicing like [:5] or [6:], but I think that would be easy enough to implement as well. Also there is no error checking for slice indexes out of range, etc.

Upvotes: 7

Related Questions