Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.lang.python > #108766 > unrolled thread

Extract the middle N chars of a string

Started bySteven D'Aprano <steve@pearwood.info>
First post2016-05-19 01:47 +1000
Last post2016-05-20 23:00 -0500
Articles 13 — 8 participants

Back to article view | Back to comp.lang.python


Contents

  Extract the middle N chars of a string Steven D'Aprano <steve@pearwood.info> - 2016-05-19 01:47 +1000
    Re: Extract the middle N chars of a string Ian Kelly <ian.g.kelly@gmail.com> - 2016-05-18 10:34 -0600
    Re: Extract the middle N chars of a string Peter Otten <__peter__@web.de> - 2016-05-18 18:41 +0200
    Re: Extract the middle N chars of a string Peter Otten <__peter__@web.de> - 2016-05-18 18:54 +0200
      Re: Extract the middle N chars of a string Steven D'Aprano <steve@pearwood.info> - 2016-05-19 11:37 +1000
    Re: Extract the middle N chars of a string MRAB <python@mrabarnett.plus.com> - 2016-05-18 18:00 +0100
      Re: Extract the middle N chars of a string Steven D'Aprano <steve@pearwood.info> - 2016-05-19 11:34 +1000
    Re: Extract the middle N chars of a string Random832 <random832@fastmail.com> - 2016-05-18 17:28 -0400
      Re: Extract the middle N chars of a string Steven D'Aprano <steve@pearwood.info> - 2016-05-19 11:03 +1000
    Re: Extract the middle N chars of a string Grant Edwards <grant.b.edwards@gmail.com> - 2016-05-18 21:35 +0000
      Re: Extract the middle N chars of a string Steven D'Aprano <steve@pearwood.info> - 2016-05-19 10:48 +1000
    Re: Extract the middle N chars of a string Wildman <best_lay@yahoo.com> - 2016-05-18 23:17 -0500
    Re: Extract the middle N chars of a string boB Stepp <robertvstepp@gmail.com> - 2016-05-20 23:00 -0500

#108766 — Extract the middle N chars of a string

FromSteven D'Aprano <steve@pearwood.info>
Date2016-05-19 01:47 +1000
SubjectExtract the middle N chars of a string
Message-ID<573c8e97$0$1596$c3e8da3$5496439d@news.astraweb.com>
Extracting the first N or last N characters of a string is easy with
slicing:

s[:N]  # first N
s[-N:]  # last N

Getting the middle N seems like it ought to be easy:

s[N//2:-N//2]

but that is wrong. It's not even the right length!

py> s = 'aardvark'
py> s[5//2:-5//2]
'rdv'


So after spending a ridiculous amount of time on what seemed like it ought
to be a trivial function, and an embarrassingly large number of off-by-one
and off-by-I-don't-even errors, I eventually came up with this:

def mid(string, n):
    """Return middle n chars of string."""
    L = len(string)
    if n <= 0:
        return ''
    elif n < L:
        Lr = L % 2
        a, ar = divmod(L-n, 2)
        b, br = divmod(L+n, 2)
        a += Lr*ar
        b += Lr*br
        string = string[a:b]
    return string


which works for me:


# string with odd number of characters
py> for i in range(1, 8):
...     print mid('abcdefg', i)
...
d
de
cde
cdef
bcdef
bcdefg
abcdefg
# string with even number of characters
py> for i in range(1, 7):
...     print mid('abcdef', i)
...
c
cd
bcd
bcde
abcde
abcdef



Is this the simplest way to get the middle N characters?



-- 
Steven

[toc] | [next] | [standalone]


#108773

FromIan Kelly <ian.g.kelly@gmail.com>
Date2016-05-18 10:34 -0600
Message-ID<mailman.10.1463589299.27390.python-list@python.org>
In reply to#108766
On Wed, May 18, 2016 at 9:47 AM, Steven D'Aprano <steve@pearwood.info> wrote:
> Extracting the first N or last N characters of a string is easy with
> slicing:
>
> s[:N]  # first N
> s[-N:]  # last N
>
> Getting the middle N seems like it ought to be easy:
>
> s[N//2:-N//2]
>
> but that is wrong. It's not even the right length!
>
> py> s = 'aardvark'
> py> s[5//2:-5//2]
> 'rdv'
>
>
> So after spending a ridiculous amount of time on what seemed like it ought
> to be a trivial function, and an embarrassingly large number of off-by-one
> and off-by-I-don't-even errors, I eventually came up with this:
>
> def mid(string, n):
>     """Return middle n chars of string."""
>     L = len(string)
>     if n <= 0:
>         return ''
>     elif n < L:
>         Lr = L % 2
>         a, ar = divmod(L-n, 2)
>         b, br = divmod(L+n, 2)
>         a += Lr*ar
>         b += Lr*br
>         string = string[a:b]
>     return string
>
>
> which works for me:
>
>
> # string with odd number of characters
> py> for i in range(1, 8):
> ...     print mid('abcdefg', i)
> ...
> d
> de
> cde
> cdef
> bcdef
> bcdefg
> abcdefg
> # string with even number of characters
> py> for i in range(1, 7):
> ...     print mid('abcdef', i)
> ...
> c
> cd
> bcd
> bcde
> abcde
> abcdef
>
>
>
> Is this the simplest way to get the middle N characters?

There's an ambiguity in the problem description when N and the length
of the string have different parity, but how about:

def mid(string, n):
    L = len(string)
    a = (L - n) // 2
    return string[a:a+n]

py> for i in range(1, 8):
...     print(mid('abcdefg', i))
...
d
cd
cde
bcde
bcdef
abcdef
abcdefg

[toc] | [prev] | [next] | [standalone]


#108774

FromPeter Otten <__peter__@web.de>
Date2016-05-18 18:41 +0200
Message-ID<mailman.11.1463589702.27390.python-list@python.org>
In reply to#108766
Steven D'Aprano wrote:

> Extracting the first N or last N characters of a string is easy with
> slicing:
> 
> s[:N]  # first N
> s[-N:]  # last N
> 
> Getting the middle N seems like it ought to be easy:
> 
> s[N//2:-N//2]
> 
> but that is wrong. It's not even the right length!
> 
> py> s = 'aardvark'
> py> s[5//2:-5//2]
> 'rdv'
> 
> 
> So after spending a ridiculous amount of time on what seemed like it ought
> to be a trivial function, and an embarrassingly large number of off-by-one
> and off-by-I-don't-even errors, I eventually came up with this:
> 
> def mid(string, n):
>     """Return middle n chars of string."""
>     L = len(string)
>     if n <= 0:
>         return ''
>     elif n < L:
>         Lr = L % 2
>         a, ar = divmod(L-n, 2)
>         b, br = divmod(L+n, 2)
>         a += Lr*ar
>         b += Lr*br
>         string = string[a:b]
>     return string
> 
> 
> which works for me:
> 
> 
> # string with odd number of characters
> py> for i in range(1, 8):
> ...     print mid('abcdefg', i)
> ...
> d
> de
> cde
> cdef
> bcdef
> bcdefg
> abcdefg
> # string with even number of characters
> py> for i in range(1, 7):
> ...     print mid('abcdef', i)
> ...
> c
> cd
> bcd
> bcde
> abcde
> abcdef
> 
> 
> 
> Is this the simplest way to get the middle N characters?

>>> def mid(s, n):
...     shave = len(s) - n
...     if shave > 0:
...         shave //= 2
...         s = s[shave:shave+n]
...     return s
... 
>>> def show(s):
...     for i in range(len(s)+1):
...         print(i, repr(s), "-->", repr(mid(s, i)))
... 
>>> show("abcdefg")
0 'abcdefg' --> ''
1 'abcdefg' --> 'd'
2 'abcdefg' --> 'cd'
3 'abcdefg' --> 'cde'
4 'abcdefg' --> 'bcde'
5 'abcdefg' --> 'bcdef'
6 'abcdefg' --> 'abcdef'
7 'abcdefg' --> 'abcdefg'
>>> show("abcdef")
0 'abcdef' --> ''
1 'abcdef' --> 'c'
2 'abcdef' --> 'cd'
3 'abcdef' --> 'bcd'
4 'abcdef' --> 'bcde'
5 'abcdef' --> 'abcde'
6 'abcdef' --> 'abcdef'

Not exactly the same results as your implementation though.

[toc] | [prev] | [next] | [standalone]


#108776

FromPeter Otten <__peter__@web.de>
Date2016-05-18 18:54 +0200
Message-ID<mailman.12.1463590511.27390.python-list@python.org>
In reply to#108766
Peter Otten wrote:

>>>> def mid(s, n):
> ...     shave = len(s) - n
> ...     if shave > 0:
              shave += len(s) % 2
> ...         shave //= 2
> ...         s = s[shave:shave+n]
> ...     return s

> Not exactly the same results as your implementation though.

The extra line should fix that, but it looks, err -- odd.

[toc] | [prev] | [next] | [standalone]


#108805

FromSteven D'Aprano <steve@pearwood.info>
Date2016-05-19 11:37 +1000
Message-ID<573d18f5$0$1593$c3e8da3$5496439d@news.astraweb.com>
In reply to#108776
On Thu, 19 May 2016 02:54 am, Peter Otten wrote:

> Peter Otten wrote:
> 
>>>>> def mid(s, n):
>> ...     shave = len(s) - n
>> ...     if shave > 0:
>               shave += len(s) % 2
>> ...         shave //= 2
>> ...         s = s[shave:shave+n]
>> ...     return s
> 
>> Not exactly the same results as your implementation though.
> 
> The extra line should fix that, but it looks, err -- odd.

Nice! Thank you. I like this solution.



-- 
Steven

[toc] | [prev] | [next] | [standalone]


#108778

FromMRAB <python@mrabarnett.plus.com>
Date2016-05-18 18:00 +0100
Message-ID<mailman.14.1463590822.27390.python-list@python.org>
In reply to#108766
On 2016-05-18 16:47, Steven D'Aprano wrote:
> Extracting the first N or last N characters of a string is easy with
> slicing:
>
> s[:N]  # first N
> s[-N:]  # last N
>
> Getting the middle N seems like it ought to be easy:
>
> s[N//2:-N//2]
>
> but that is wrong. It's not even the right length!
>
> py> s = 'aardvark'
> py> s[5//2:-5//2]
> 'rdv'
>
>
> So after spending a ridiculous amount of time on what seemed like it ought
> to be a trivial function, and an embarrassingly large number of off-by-one
> and off-by-I-don't-even errors, I eventually came up with this:
>
> def mid(string, n):
>     """Return middle n chars of string."""
>     L = len(string)
>     if n <= 0:
>         return ''
>     elif n < L:
>         Lr = L % 2
>         a, ar = divmod(L-n, 2)
>         b, br = divmod(L+n, 2)
>         a += Lr*ar
>         b += Lr*br
>         string = string[a:b]
>     return string
>
>
> which works for me:
>
>
> # string with odd number of characters
> py> for i in range(1, 8):
> ...     print mid('abcdefg', i)
> ...
> d
> de
> cde
> cdef
> bcdef
> bcdefg
> abcdefg
> # string with even number of characters
> py> for i in range(1, 7):
> ...     print mid('abcdef', i)
> ...
> c
> cd
> bcd
> bcde
> abcde
> abcdef
>
>
>
> Is this the simplest way to get the middle N characters?
>
I think your results are inconsistent.

For an odd number of characters you have "abc" + "de" + "fg", i.e. more 
on the left, but for an even number of characters you have "a" + "bcd" + 
"ef", i.e. more on the right.


My own solution is:


def mid(string, n):
     """Return middle n chars of string."""
     if n <= 0:
         return ''

     if n > len(string):
         return string

     ofs = (len(string) - n) // 2

     return string[ofs : ofs + n]


If there's an odd number of characters remaining, it always has more on 
the right.

[toc] | [prev] | [next] | [standalone]


#108804

FromSteven D'Aprano <steve@pearwood.info>
Date2016-05-19 11:34 +1000
Message-ID<573d1844$0$1593$c3e8da3$5496439d@news.astraweb.com>
In reply to#108778
On Thu, 19 May 2016 03:00 am, MRAB wrote:

> I think your results are inconsistent.
> 
> For an odd number of characters you have "abc" + "de" + "fg", i.e. more
> on the left, but for an even number of characters you have "a" + "bcd" +
> "ef", i.e. more on the right.

Correct. That's intentional.

I didn't start with an algorithm. I started by manually extracting the N
middle characters, for various values of N, then wrote code to get the same
result.

For example, if I start with an odd-length string, like "inquisition", then
the "middle N" cases for odd-N are no-brainers, because they have to be
centered on the middle character:

N=1 's'
N=3 'isi'
N=5 'uisit'

For even-N, I had a choice:

N=2 'is' or 'si'

and to be perfectly frank, I didn't really care much either way and just
arbitrarily picked the second, based on the fact that string.center() ends
up with a slight bias to the right:

py> 'ab'.center(5, '*')
'**ab*'


For even-length string, like "aardvark", the even-N case is the no-brainer:

N=2 "dv"
N=4 "rdva"
N=6 "ardvar"

but with odd-N I have a choice:

N=3 "rdv" or "dva"

In this case, I *intentionally* biased it the other way, so that (in some
sense) overall the mid() function would be unbiased:

- half the cases, there's no bias at all;
- a quarter of the time, there's a bias to the right;
- a quarter of the time, there's a bias to the left;
- so on average, the bias is zero.


> My own solution is:
> 
> 
> def mid(string, n):
>      """Return middle n chars of string."""
>      if n <= 0:
>          return ''
>      if n > len(string):
>          return string
>      ofs = (len(string) - n) // 2
>      return string[ofs : ofs + n]
> 
> 
> If there's an odd number of characters remaining, it always has more on
> the right.


Thanks to you and Ian (who independently posted a similar solution), that's
quite good too if you don't care about the bias.



-- 
Steven

[toc] | [prev] | [next] | [standalone]


#108785

FromRandom832 <random832@fastmail.com>
Date2016-05-18 17:28 -0400
Message-ID<mailman.19.1463606931.27390.python-list@python.org>
In reply to#108766
On Wed, May 18, 2016, at 11:47, Steven D'Aprano wrote:
> So after spending a ridiculous amount of time on what seemed like it
> ought
> to be a trivial function, and an embarrassingly large number of
> off-by-one
> and off-by-I-don't-even errors, I eventually came up with this:
> 
> def mid(string, n):
>     """Return middle n chars of string."""
>     L = len(string)
>     if n <= 0:
>         return ''
>     elif n < L:
>         Lr = L % 2
>         a, ar = divmod(L-n, 2)
>         b, br = divmod(L+n, 2)
>         a += Lr*ar
>         b += Lr*br
>         string = string[a:b]
>     return string

My take:

def mid(string, n):
    if n > len(string): n = len(string)
    if n <= 0: n = 0
    offset = int(len(string)/2+n/2)
    return string[offset:offset+n]

It doesn't get the same result as yours when the length is odd and N is
even, but the problem is ambiguous there and I feel like my algorithm is
more clear.

I'm funneling the special cases through the slice statement at the end
rather than simply returning string and '' because it's conceptually
nicer and because it could be used for other purposes than slicing
strings.

[toc] | [prev] | [next] | [standalone]


#108799

FromSteven D'Aprano <steve@pearwood.info>
Date2016-05-19 11:03 +1000
Message-ID<573d10e0$0$1612$c3e8da3$5496439d@news.astraweb.com>
In reply to#108785
On Thu, 19 May 2016 07:28 am, Random832 wrote:

> My take:
> 
> def mid(string, n):
>     if n > len(string): n = len(string)
>     if n <= 0: n = 0
>     offset = int(len(string)/2+n/2)
>     return string[offset:offset+n]
> 
> It doesn't get the same result as yours when the length is odd and N is
> even, but the problem is ambiguous there and I feel like my algorithm is
> more clear.

I'm not sure that "the algorithm is more clear" is the right way to judge
this. Especially when your function doesn't even come *close* to meeting
the requirements. It doesn't even return substrings of the right length!

py> for i in range(8):
...     print mid('abcdefg', i)
...
<BLANKLINE>
e
ef
fg
fg
g
g
<BLANKLINE>
py> for i in range(7):
...     print mid('abcdef', i)
...
<BLANKLINE>
d
ef
ef
f
f
<BLANKLINE>


I sympathise, I really do, because as my first post says, this really does
seem like it ought to be a no-brainer trivial exercise in slicing. Instead,
it is remarkably subtle and tricky to get right.




-- 
Steven

[toc] | [prev] | [next] | [standalone]


#108786

FromGrant Edwards <grant.b.edwards@gmail.com>
Date2016-05-18 21:35 +0000
Message-ID<mailman.20.1463607348.27390.python-list@python.org>
In reply to#108766
On 2016-05-18, Steven D'Aprano <steve@pearwood.info> wrote:

> Getting the middle N seems like it ought to be easy:

I'm still trying to figure out when one would want to do that...

-- 
Grant Edwards               grant.b.edwards        Yow! My CODE of ETHICS
                                  at               is vacationing at famed
                              gmail.com            SCHROON LAKE in upstate
                                                   New York!!

[toc] | [prev] | [next] | [standalone]


#108795

FromSteven D'Aprano <steve@pearwood.info>
Date2016-05-19 10:48 +1000
Message-ID<573d0d53$0$1598$c3e8da3$5496439d@news.astraweb.com>
In reply to#108786
On Thu, 19 May 2016 07:35 am, Grant Edwards wrote:

> On 2016-05-18, Steven D'Aprano <steve@pearwood.info> wrote:
> 
>> Getting the middle N seems like it ought to be easy:
> 
> I'm still trying to figure out when one would want to do that...

I wanted to centre some text and truncate it to a fixed width. So I needed
the middle N characters.



-- 
Steven

[toc] | [prev] | [next] | [standalone]


#108816

FromWildman <best_lay@yahoo.com>
Date2016-05-18 23:17 -0500
Message-ID<l8udnZMUVeDdo6DKnZ2dnUU7-fmdnZ2d@giganews.com>
In reply to#108766
On Thu, 19 May 2016 01:47:33 +1000, Steven D'Aprano wrote:

> Is this the simplest way to get the middle N characters?

This will return a sub-string of any length starting at any
point.  This is the way the old VB mid$ function worked.

def mid(string, start, length):
    # start begins at 0
    if string = "":
        return None
    if start > len(string) - 1:
        start = len(string) - 1
    if length > len(string):
        length = len(string)
    if length < 1:
        length = 1
    return string[start : start + length]

-- 
<Wildman> GNU/Linux user #557453
"The Constitution only gives people the right to
pursue happiness. You have to catch it yourself."
  -Benjamin Franklin

[toc] | [prev] | [next] | [standalone]


#108891

FromboB Stepp <robertvstepp@gmail.com>
Date2016-05-20 23:00 -0500
Message-ID<mailman.69.1463803242.27390.python-list@python.org>
In reply to#108766
On Wed, May 18, 2016 at 10:47 AM, Steven D'Aprano <steve@pearwood.info> wrote:

> Getting the middle N seems like it ought to be easy:
>
> s[N//2:-N//2]
>
> but that is wrong. It's not even the right length!
>
> py> s = 'aardvark'
> py> s[5//2:-5//2]
> 'rdv'
>
>
> So after spending a ridiculous amount of time on what seemed like it ought
> to be a trivial function, and an embarrassingly large number of off-by-one
> and off-by-I-don't-even errors, I eventually came up with this:
>
> def mid(string, n):
>     """Return middle n chars of string."""
>     L = len(string)
>     if n <= 0:
>         return ''
>     elif n < L:
>         Lr = L % 2
>         a, ar = divmod(L-n, 2)
>         b, br = divmod(L+n, 2)
>         a += Lr*ar
>         b += Lr*br
>         string = string[a:b]
>     return string

As some of you know, I usually post on the Tutor list while attempting
to learn Python as time permits.  I had to try my hand at this problem
as a learning opportunity.  I hope you don't mind if I explain how I
got to my solution and welcome your critiques, so I may improve.  I
chose to cheat my answers to the right; I did not think about the
possibility of alternating the sides to allot the extra character
(when needed) to average things out until I read everyone's answers
after getting my own.

I started considering two strings, s_even = '0123456789' and s_odd =
'123456789', with trial values of n = 4 and n = 5 for how many
characters to extract.  This gave me the following four desired
outputs to replicate:

1)  s_even with n = 5.  Desired output:  '34567' (Cheating right.)  =>
Slice s_even[3:8]
2)  s_even with n = 4.  Desired output:  '3456' (Exact.)  => Slice s_even[3:7]
3)  s_odd with n = 5.  Desired output:  '34567' (Exact.)  => Slice s_odd[2:7]
4)  s_odd with n = 4.  Desired output:  '4567' (Cheating right.)  =>
Slice s_odd[3:7]

Starting to generalize to get the desired indices for each case:

1)  (len(s_even)//2 - n//2):(len(s_even)//2 + n//2 + 1)
2)  (len(s_even)//2 - n//2):(len(s_even)//2 + n//2)
3)  (len(s_odd)//2 - n//2):(len(s_odd)//2 + n//2 + 1)
4)  (len(s_odd)//2 + 1 - n//2):(len(s_odd)//2 + n//2 + 1)

Looking at the starting index for each case, I had an extra 1 for case
(4), which, in table form:

        n even    n odd
s_even    0         0
s_odd     1         0

To duplicate this I came up with the expression:  (len(s)%2) * (1 - n%2)

Similarly, for the ending slice index, all cases have an extra "+ 1"
except for case (2), with the following table:

        n even    n odd
s_even    0         1
s_odd     1         1

And the expression:  1 - ((len(s) + 1)%2 * (n +1)%2)

All this was scribbled onto scratch paper, so I hope I did not make
any typos!  This led me to the following code:

py3: def mid(s, n):
...     index0_offset = (len(s)%2) * (1 - n%2)
...     index1_offset = 1 - ((len(s) + 1)%2) * ((n + 1)%2)
...     index0 = len(s)//2 - n//2 + index0_offset
...     index1 = len(s)//2 + n//2 + index1_offset
...     return s[index0:index1]
...
py3: s = '0123456789'
py3: n = 5
py3: mid(s, n)
'34567'
py3: n = 4
py3: mid(s, n)
'3456'
py3: s = '123456789'
py3: n = 5
py3: mid(s, n)
'34567'
py3: n = 4
py3: mid(s, n)
'4567'
py3: s = 'aardvark'
py3: n = 5
py3: mid(s, n)
'rdvar'

This also returns an empty string for values of n <= 0.

As far as I can tell, my solution works (Given cheating right.).  I
ran it on all of Steve's examples, and I got what I expected given
that I am consistently cheating right.  But I am not sure my code
adequately conveys an understanding of what I am doing to the casual
reader.  Thoughts?

TIA!
boB

[toc] | [prev] | [standalone]


Back to top | Article view | comp.lang.python


csiph-web