11
\$\begingroup\$

I have a long piece of code that has many repeating points. I am sure that it doesn't take this amount of code to create a simple calendar, but I do not know how to shorten this piece of code.

# Creating a calendar in proportion to the user defined starting date

n = int(input('Enter number of days: '))#days in a month
s = input('The first day of the week: ')#The date that user wants the month to start
s = s.capitalize()

lis1 = ['Sa','M','T','W','Th','F','Su']
print('{:>3}{:>4}{:>4}{:>4}{:>4}{:>4}{:>4}'.format('Sa','M','T','W','Th','F','Su'))

for i in range(1,n+1):#loop to generate the dates for a month
  if s==lis1[0] and i ==1:#this is to determine how much should the code indent to align to traditional calendar formatting
    print('{:>3}'.format(i),end =' ')
  elif s==lis1[1] and i ==1:
    print(' '*4+'{:>3}'.format(i),end =' ')
  elif s==lis1[2] and i ==1:
    print(' '*8+'{:>3}'.format(i),end =' ')
  elif s==lis1[3] and i ==1:
    print(' '*12+'{:>3}'.format(i),end =' ')
  elif s==lis1[4] and i ==1:
    print(' '*16+'{:>3}'.format(i),end =' ')
  elif s==lis1[5] and i ==1:
    print(' '*20+'{:>3}'.format(i),end =' ')
  elif s==lis1[6] and i ==1:
    print(' '*24+'{:>3}'.format(i),end =' ')
  else:
    print('{:>3}'.format(i),end =' ')#after the indent this is to print the remaining dates

  if s==lis1[0] and i%7==0:#this is to print a new line in proportion the choosen starting date so as to stay in check with the traditional calendar format
    print()
  elif s==lis1[1] and i%7==6:
    print()
  elif s==lis1[2] and i%7==5:
    print()
  elif s==lis1[3] and i%7==4:
    print()
  elif s==lis1[4] and i%7==3:
    print()
  elif s==lis1[5] and i%7==2:
    print()
  elif s==lis1[6] and i%7==1:
    print()

This is an example of the calendar I am outputting.

Calendar for May 2010

Please guide me on shortening code that is related to loops if possible.

\$\endgroup\$
  • \$\begingroup\$ You may be interested in Pythons calendar module. For example a simple text calendar for May 2010 with Sunday as the start of the week could be: import calendar; calendar.setfirstweekday(6); print(calendar.month(2010, 5)) \$\endgroup\$ – Peilonrayz Jul 5 '17 at 14:57
7
\$\begingroup\$

First improvement

don't repeat yourself, the dry principle

You have many elif statements that can be easily rewritten using a dictionary.

My final program has none at all. I rewrote your lst (also badly named) into a dictionary like this:

weekdays = {'Su':0,'M':1,'T':2,'W':3,'Th':4,'F':5,'Sa':6}

Now weekdays is used on 2 different locations to replace the long elif structure.

First elif block. Notice they all increment by 4. So I can use the value of the dictionary like this: weekdays[first_weekday]*4

Second elif block. Here you can use the same dictionary. Notice how weekdays[first_weekday] + current_day becomes a multitude of 7.

Second Improvement

You should rename the variables in your program to understand what they do. For example n should be renamed to number_of_days

A good way to follow are the PEP8 guidelines

Final Program

As you can see, this program not only is shorter but also far more readable

# I omitted comments because the names speak for themselves
number_of_days = int(input('Enter number of days in month: '))
first_weekday = input('First weekday of the month: ')

# Using a dictionary over a list is the biggest improvement in my opinion
weekdays = {'Su': 0, 'M': 1, 'T': 2, 'W': 3, 'Th': 4, 'F': 5, 'Sa': 6}
print('{:>3}{:>4}{:>4}{:>4}{:>4}{:>4}{:>4}'
      .format('Su', 'M', 'T', 'W', 'Th', 'F', 'Sa'))

# First elif block
print (weekdays[first_weekday]*4*' ' + '{:>3}'.format(1), end=' ')
for current_day in range(1, number_of_days+1):
    # Second elif block
    if (weekdays[first_weekday] + current_day) % 7 == 0:
        print ()
    print ('{:>3}'.format(current_day ), end=' ')
\$\endgroup\$
  • \$\begingroup\$ I think there is a bug in your program, just try 20 and 'Su' as input. \$\endgroup\$ – mcocdawc Jul 5 '17 at 14:35
  • \$\begingroup\$ In your days_of_week, Saturday and Sunday are swapped \$\endgroup\$ – Graipher Jul 5 '17 at 14:47
  • \$\begingroup\$ Thanks man for the thoughtful write out! I was wondering if the same can be achieved using just using nested loop and not having to use a dictionary? \$\endgroup\$ – low kim Jul 5 '17 at 22:16
1
\$\begingroup\$

The first important rule of thumb is always: If possible use libraries that deal with your problem. Probably there is a calendar library, or you can use whitespace CSV printing to print column wise oriented text.

Under the assumption that you want to implement it without libraries there are some improvements. Some of them are very general, some are python specific.

  1. Formatting (python specific)

Read the pep 8 style guide and format your code appropiately.

  1. Separate functionality

It is always a good idea to have separate functions for separate operations like reading input, calculating, outputting results...

  1. Less global state

If you use functions write them in a way, that you don't need global variables.

  1. Use list comprehensions (python specific)

Just like dictionaries in the answer of Ludisposed list comprehensions are bread and butter in python. It helps to get familiar with them early.

  1. Self documenting code + better variable names

There is a simple rule: avoid comments!

If you have a line like the following:

n = int(input('Enter number of days in a month: ))#days in a month

The user won't know, if it is the number of days in a week or in a month, because the user should not be forced to look into your source code.

If you write instead:

n = int(input('Enter number of days in a month:))

You don't need comments in the source code and the user knows what to type. One could argue that this should be even extended:

n_days_in_month = int(input('Enter number of days in a month:))

Then you definitely won't need a comment even lateron.

Taking everything into account you can rewrite it like this:

days = ['Su','M','T','W','Th','F','Sa']

def get_user_input():
    while True:
        message = 'Enter number of days in a month (has to be between 1 and 31): '
        n_days_in_month = int(input(message))
        if 1 <= n_days_in_month <= 31:
            break
    while True:
        message = "The first day of the week (has to be one of the following {}): ".format(days)
        first_day = input(message)
        if first_day in days:
            break
    return n_days_in_month, first_day

def reshape(n_days_in_month, first_day):
    i = days.index(first_day)
    n_weeks = (n_days_in_month // 7) + 1 + i
    rows_of_days = []
    rows_of_days.append(['' for _ in range(i)] + list(range(1, 7 - i + 1)))
    last = 7 - i + 1
    for week in range(1, n_weeks):
        rows_of_days.append(range(last, min(last + 7, n_days_in_month + 1)))
        last += 7 
    return rows_of_days

def output_calendar(rows_of_days):
    print(*['{:>3}'.format(x) for x in days])
    for row in rows_of_days:
        print(*['{:>3}'.format(x) for x in row])

n_days_in_month, first_day = get_user_input()

output_calendar(reshape(n_days_in_month, first_day))
\$\endgroup\$
  • \$\begingroup\$ Thx man! Though I have not learnt how to use function in python yet, but I am sure this will serve as an great example. \$\endgroup\$ – low kim Jul 5 '17 at 22:43

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.