4
\$\begingroup\$

I am working on this coding problem:

Count the number of words & lines that have more than X vowels for every Y words in every Z line.

Basically the input string has multiple lines and I need to count words that have X or more vowels in them. But the constraint is I need to consider only every Zth line as well as only every Yth word in those lines.

For example let's say I need to count every 3rd word that has 2 or more vowels in every 3rd line. So here X=2, Y=3, Z=3.

Input string:

"1.When I first brought my cat home.
 2.It cost a lot to adopt her.
 3.I paid forty dollars for it.
 4.And then I had to buy litter, a litterbox.
 5.Also bought food, and dishes for her to eat out of. 
 6.There's a **leash** law for cats in Fort **Collins**.
 7.If they're not in your yard they have to be on a leash. 
 8.Anyway, my cat is my best friend. 
 9.I'm glad I got her. 
 10.She sleeps under the covers with me when it's cold."

Output should be:

Word Count: 2, Line Count: 1

So based on criteria of Z=3, i.e. "count every 3rd line", the lines to be considered are lines 3, 6, 9.

In these lines we need to count Y=3, i.e. every 3rd word. So the words to be considered are forty, it from line 3, leash, cats, Collins from line 6, and I from line 9.

Given these criteria, the matching words with 2 or more vowels are found only in line 6 with words leash and Collins, so the output is WordCount = 2 and LineCount = 1.

class StringCount:  

  lines = list();
  totalMatchedLines = 0;
  totalMatchedWords = 0;
  matchedChars = 0;

  def __init__(self, inputString, vowelCount, skipWords, skipLines, wordDelimiter, lineDelimiter):
    self.inputString = inputString;
    self.vowelCount = vowelCount;
    self.skipWords = skipWords;
    self.skipLines = skipLines;
    self.wordDelimiter = wordDelimiter;

  def splitLines(self):
    if self.inputString.strip() == "":
      print ("Please enter a valid string!");
      return False;      
    self.lines = self.inputString.splitlines();

  def splitWords(self):      
    self.matchedWords = 0;
    self.matchedLines = 0;
    self.linesLength = len(self.lines);

    if self.linesLength < self.skipLines:
      print ("Input string should be greater than {0}" .format(self.skipLines));
      return False;

    lineCount = self.skipLines - 1;
    wordCount = self.skipWords - 1;
    lineInUse = "";
    words = list();

    while (lineCount < self.linesLength):
      self.matchedWords = 0;
      self.matchedLines = 0;
      self.words = self.lines[lineCount].split();
      self.wordsLength = len(self.words);
      wordCount = self.skipWords - 1;

      while (wordCount < self.wordsLength):
        self.matchedChars = 0;       
        for i in self.words[wordCount].lower():
          if(i=='a' or i=='e' or i=='i' or i=='o' or i=='u'):
            self.matchedChars += 1;              
        if self.matchedChars >= self.vowelCount:
          self.matchedWords += 1;
        wordCount += self.skipWords;

      if self.matchedWords > 0:
        self.matchedLines += 1;

      self.totalMatchedWords += self.matchedWords;
      self.totalMatchedLines += self.matchedLines;
      lineCount += self.skipLines;

    print ("WordCount = %s" % (self.totalMatchedWords));
    print ("LineCount = %s" % (self.totalMatchedLines));

Code to invoke the class:

mystring = "1.When I first brought my cat home.
 2.It cost a lot to adopt her.
 3.I paid forty dollars for it.
 4.And then I had to buy litter, a litterbox.
 5.Also bought food, and dishes for her to eat out of. 
 6.There's a **leash** law for cats in Fort **Collins**.
 7.If they're not in your yard they have to be on a leash. 
 8.Anyway, my cat is my best friend. 
 9.I'm glad I got her. 
 10.She sleeps under the covers with me when it's cold.";

myobjectx = StringCount(myString, 2, 3, 3, " ", "\n");
myobjectx.splitLines();
myobjectx.splitWords();

Since this is my first piece of Python code, I wanted to check how to optimize this code in both performance and line optimization. Is there any inbuilt functionality to shorten the multiple while loops and the for loop?

\$\endgroup\$
  • 1
    \$\begingroup\$ optimize this code in both performance and line optimization readability (→maintainability) first! \$\endgroup\$ – greybeard May 24 at 19:44
  • \$\begingroup\$ @200_success, just updated the post with invocation code. \$\endgroup\$ – Naphstor May 24 at 23:52
  • 1
    \$\begingroup\$ For the record, mystring spans multiple lines, and needs to be triple-quoted. \$\endgroup\$ – 200_success May 25 at 0:30
3
\$\begingroup\$

Style

PEP 8 is the official style guide for Python. Unless you have a good reason to deviate, you should use lower_case_with_underscores for variable names and method names. Terminating statements with semicolons is a faux pas in Python. Also, indentation should be four spaces, which is a particularly important convention to follow in Python since whitespace is significant.

Interface and Behavior

The wordDelimiter and lineDelimiter parameters are completely ignored.

It's not obvious that the proper way to invoke this code is to call .splitLines() followed by .splitWords(). It's even more surprising that .splitWords() prints anything at all — from the name of the method, I wouldn't have guessed that it would have that effect. In contrast, my solution below makes it more obvious what the program does:

wc, lc = StringCounter(2, 3, 3).word_and_line_counts(fileinput.input())
print("Word count = {0}".format(wc))
print("Line count = {0}".format(lc))

In my opinion, an input that is an empty string or that has an insufficient number of lines should not be an error. After all, a line that contains too few words isn't an error, right?

Consider accepting the input as a line iterator, rather than as a string. Python will naturally give you a line iterator if you read from a file or from sys.stdin.

Eloquent looping and filtering

In my solution below, I have defined three lambda expressions to help with the iterating and counting. Defining these three short functions helps make the code's purpose more obvious.

A lot of loops in Python can be written very expressively using the itertools module. In particular, itertools.islice(iterable, start, stop, step) will skip to every nth element.

Counting can be done using the sum() built-in function with a generator expression. Furthermore, you can take advantage of the fact that True and False are treated as 1 and 0, respectively, when adding. My line_count += (matched_words > 0) is equivalent to your

if self.matchedWords > 0:
  self.matchedLines += 1;

My sum((c in 'aeiouAEIOU') for c in word) >= x is equivalent to your

for i in self.words[wordCount].lower():
  if(i=='a' or i=='e' or i=='i' or i=='o' or i=='u'):
    self.matchedChars += 1;              
if self.matchedChars >= self.vowelCount:
  self.matchedWords += 1;

Suggested solution

from itertools import islice

class StringCounter:
    def __init__(self, x, y, z):
        # Function that takes lines and yields every zth line
        self.candidate_lines = lambda lines: islice(lines, z - 1, None, z)

        # Function that takes one line and yields every yth word in it
        self.candidate_words = lambda line: islice(line.split(), y - 1, None, y)

        # Function that takes words and counts how many of them have >= x vowels
        self.vowel_rich_words = lambda words: sum(
             sum((c in 'aeiouAEIOU') for c in word) >= x
             for word in words
        )

    def word_and_line_counts(self, line_iter):
        word_count = line_count = 0
        for line in self.candidate_lines(line_iter):
            matched_words = self.vowel_rich_words(self.candidate_words(line))
            word_count += matched_words
            line_count += (matched_words > 0)
        return word_count, line_count

mystring = """1.When I first brought my cat home.
 2.It cost a lot to adopt her.
 3.I paid forty dollars for it.
 4.And then I had to buy litter, a litterbox.
 5.Also bought food, and dishes for her to eat out of. 
 6.There's a **leash** law for cats in Fort **Collins**.
 7.If they're not in your yard they have to be on a leash. 
 8.Anyway, my cat is my best friend. 
 9.I'm glad I got her. 
 10.She sleeps under the covers with me when it's cold."""

wc, lc = StringCounter(2, 3, 3).word_and_line_counts(mystring.splitlines())
print("Word count = {0}".format(wc))
print("Line count = {0}".format(lc))
\$\endgroup\$
  • \$\begingroup\$ thanks for the explanation. I have never used python before so wrote this basic solution as per basic functions I learned in tutorials. But this is nicely explained. Thanks again. \$\endgroup\$ – Naphstor May 26 at 2:47

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.