22
\$\begingroup\$

If anything in the text isn't a letter, ignore it and don't return it.

a being 1, b being 2, etc.

As an example:

alphabet_position("The sunset sets at twelve o' clock.") Should return "20 8 5 19 21 14 19 5 20 19 5 20 19 1 20 20 23 5 12 22 5 15 3 12 15 3 11" as a string.

It is my naive solution and my code is below in python 2.7

def alphabet_position(text):
    dictt = {'a':'1','b':'2','c':'3','d':'4','e':'5','f':'6','g':'7','h':'8',
    'i':'9','j':'10','k':'11','l':'12','m':'13','n':'14','o':'15','p':'16','q':'17',
    'r':'18','s':'19','t':'20','u':'21','v':'22','w':'23','x':'24','y':'25','z':'26'
    }
    arr = []
    new_text = text.lower()
    for i in list(new_text):
        for k, j in dictt.iteritems():
            if k == i:
                arr.append(j)
    return ' '.join(arr)
\$\endgroup\$
  • 1
    \$\begingroup\$ ...why are you iterating over a dictionary instead of using the accessor? \$\endgroup\$ – Fund Monica's Lawsuit Dec 27 '17 at 6:07
  • 1
    \$\begingroup\$ What should happen to á, ß, ç, Ø, θ or ж ? \$\endgroup\$ – Pieter B Dec 27 '17 at 8:27
  • \$\begingroup\$ @PieterB in this specific case it ignores those letters, since the goal is to get only alphabet letters(English). \$\endgroup\$ – nexla Dec 27 '17 at 9:44
  • \$\begingroup\$ @nexla sorry for being nit-picking, I saw what the code did, but getting clear requirement is always the start of having good software. \$\endgroup\$ – Pieter B Dec 27 '17 at 10:05
27
\$\begingroup\$

First of all, you don't need to hardcode the letters and their positions in the alphabet - you can use the string.ascii_lowercase.

Also, you don't have to call list() on a new_text - you can just iterate over it character by character.

Then, what if we would construct a mapping between letters and letter indexes in the alphabet (with the help of enumerate()). Then, use a list comprehension to create an array of numbers which we then join to produce a result:

from string import ascii_lowercase


LETTERS = {letter: str(index) for index, letter in enumerate(ascii_lowercase, start=1)} 

def alphabet_position(text):
    text = text.lower()

    numbers = [LETTERS[character] for character in text if character in LETTERS]

    return ' '.join(numbers)
\$\endgroup\$
  • \$\begingroup\$ Would [LETTERS.get(character) for character in text] be better? Also, if we want to optimize for speed, I think that for a long text it would be better to generate a dictionary for upper and lower case letters upfront and then save time not doing lower() for each letter (after all, you're essentially iterating over the string twice, once to put everything in lower case, and again to convert to cumbers). \$\endgroup\$ – Acccumulation Dec 27 '17 at 17:50
  • \$\begingroup\$ @Acccumulation I like the idea of not lowering the string, classic space-time sacrifice, thanks! \$\endgroup\$ – alecxe Dec 27 '17 at 18:06
  • \$\begingroup\$ Minor nitpick: numbers could be a generator : numbers = (LETTERS[character] for character in text if character in LETTERS) \$\endgroup\$ – Eric Duminil Dec 27 '17 at 21:32
  • \$\begingroup\$ @EricDuminil It could, but last time I checked, joining a list was slightly faster than joining a generator, because in cPython str.join first consumes the generator into a list, internally. It needs to do that to know how much space to allocate for the string. See for example stackoverflow.com/a/37782238. \$\endgroup\$ – Graipher Dec 28 '17 at 11:04
11
\$\begingroup\$

Another slightly different approach than what @alecxe proposed (well, not so different ^_^), would be to use Python's builtins count and zip to generate the mapping between letters and their position in the alphabet.

from itertools import count
from string import ascii_lowercase


def letter_indexes(text):
    letter_mapping = dict(zip(ascii_lowercase, count(1)))
    indexes = [
      letter_mapping[letter] for letter in text.lower() 
      if letter in letter_mapping
    ]

    return ' '.join(str(index) for index in indexes)
\$\endgroup\$
7
\$\begingroup\$

If you only care about ASCII characters, you can also exploit the fact that their character codes go from 97 for 'a' to 122 for 'z', contiguously, and do something like

def alphabet_position(text):
    nums = [str(ord(x) - 96) for x in text.lower() if x >= 'a' and x <= 'z']
    return " ".join(nums)

Note, however, that it may give an impression of being faster than the @alecxe's solution, but is, in fact, quite a lot slower for long input strings, because calling str() and ord() on every input character is slower than dictionary lookup. Gives about the same or even slightly better performance for repeated calls on short input strings, but only because letters dictionary is constructed anew on every call of @alecxe's function, which is easy to change. (UPD.: no, not anymore). If that matters.

\$\endgroup\$
  • \$\begingroup\$ Good point about the letters dictionary being re-initialized on every call - moved to a proper “constant”. Thanks! \$\endgroup\$ – alecxe Dec 27 '17 at 3:58
  • \$\begingroup\$ I'm too lazy to write speed tests, but I'd imagine that bytes( ... generate bytes ... ).encode('ascii') would be faster than calling str on each character. \$\endgroup\$ – wvxvw Dec 27 '17 at 6:23
  • \$\begingroup\$ The magic number in ord(x) - 96 is kind of funny - use ord(x) - ord('a') + 1 or offsetA = ord('a') - 1 ord(x) - offsetA. \$\endgroup\$ – greybeard Mar 16 at 5:35
5
\$\begingroup\$

Finally, after a lot of head-banging, I found a way to avoid calling ord(), which, apparently, is very expensive. Below is the test code and results:

from timeit import timeit
from itertools import count
from string import ascii_lowercase


def alphabet_position_Headcrab(text):
    nums = [str(ord(x) - 96) for x in text.lower() if x >= 'a' and x <= 'z']
    return " ".join(nums)


def alphabet_position_wvxvw(text):
    result, i = [32, 32, 32] * len(text), 0
    for c in bytes(text.lower(), 'ascii'):
        if 97 <= c < 106:
            result[i] = c - 48
            i += 2
        elif 106 <= c < 116:
            result[i] = 49
            result[i + 1] = c - 58
            i += 3
        elif 116 <= c <= 122:
            result[i] = 50
            result[i + 1] = c - 68
            i += 3
    return bytes(result[:i-1])


def letter_indexes(text):
    text = text.lower()

    letter_mapping = dict(zip(ascii_lowercase, count(1)))
    indexes = [
      letter_mapping[letter] for letter in text
      if letter in letter_mapping
    ]

    return ' '.join(str(index) for index in indexes)


def test(f):
    data = "The sunset sets at twelve o' clock."
    for _ in range(5):
        f(data)
        data = data + data


def speed_compare():
    results = {
        'wvxvw': timeit(
            'test(alphabet_position_wvxvw)',
            setup='from __main__ import (test, alphabet_position_wvxvw)',
            number=10000,
        ),
        'Headcrab': timeit(
            'test(alphabet_position_Headcrab)',
            setup='from __main__ import (test, alphabet_position_Headcrab)',
            number=10000,
        ),
        'MrGrj': timeit(
            'test(letter_indexes)',
            setup=(
                'from __main__ import (test, letter_indexes)\n'
                'from itertools import count\n'
                'from string import ascii_lowercase\n'
            ),
            number=10000,
        )
    }
    for k, v in results.items():
        print(k, 'scored', v)

Running speed_compare() gives this output:

wvxvw scored 1.7537127458490431
Headcrab scored 2.346936965826899
MrGrj scored 2.2078608609735966
\$\endgroup\$
  • 3
    \$\begingroup\$ I guess this is a solution to the problem, but in my taste it is way too ambiguous and complicated for the relatively simple problem. \$\endgroup\$ – Daniel Dec 27 '17 at 10:01
  • \$\begingroup\$ Isn't the solution given by @alecxe still faster? When I checked it on an English translation of "Anna Karenina", it was about 3 times faster than my code, by that estimate should be faster than yours, too. \$\endgroup\$ – Headcrab Dec 27 '17 at 10:56
  • \$\begingroup\$ @Headcrab I don't know how to measure it: it has non-trivial setup code, which is also part of the solution to the problem. If I include this setup code in each test, then it won't be much different from MrGrj's code, else, it feels unfair because some work needed to solve the problem is being done outside the test. \$\endgroup\$ – wvxvw Dec 27 '17 at 11:15
  • \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ – Toby Speight May 30 at 12:11
-3
\$\begingroup\$

Seems complicated with all the libraries requirements for this simple problem. I'm not proficient in Python syntax but I know this can be done. Let me explain in pseudocode (syntax can be looked up by you):

  1. Declare a list (or array as I call it in other langs) of alphabet[]="'a', 'b',....'z'" Their index position is ALREADY their positions...so, the position of 'a' is 0 (because Python index is 0-based; if you don't like it, you can just add 1 to the result to show results counting from 1) (lowercase or uppercase, doesn't matter since you're looking at the position of them in either case not their ord values)
  2. Get the string to compare and its length: len(astring) For that length, iterate and find the character (achar for example) in the string (astring for example). Either for loop or some magical python 1-line statements like: for achar in astring If you use a 1-line statement instead of a loop, you'd probably have to load them into another list and loop in that. Not sure, I like for loops, so... Inside the for loop, check each achar's index location if chr(x) == astring[i] for example where i is loop counter, then load that location into a new array (define an empty array outside of the loop first) say, "positions[]"
  3. After the loop ends, positions[] should have the positions (0-based) of each matching char. You could add 1 to each value if you want the positions to be 1-based.

Sorry, I couldn't write the whole code mainly because my syntax knowledge isn't very good in Python yet, but the logic should make sense, right?

\$\endgroup\$
  • 3
    \$\begingroup\$ Welcome to CR Tony, This answer doesn't look like it is reviewing code, and your answer is not readable. Please see editing help. \$\endgroup\$ – bhathiya-perera Mar 16 at 0:44
  • \$\begingroup\$ the logic [presented above] should make sense maybe it should: in my eyes, it doesn't. Prefer readability, avoid open code. Bother about speed when problems arise, but iterate and find the character […] in the string looks asking for it. (Wait, I don't even get what it is you are suggesting: what would achar be, and how would finding it contribute to the result wanted?) \$\endgroup\$ – greybeard Mar 16 at 5:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.