Per R P Herrold's challenge.

The hardest part was figuring out what the program was supposed to accomplish.

From the email, code documentation, and code itself, I had a very difficult time understanding what the code was trying to accomplish, nevermind how the code was accomplishing it.

After studying the code, I made my description below of what the code is to accomplish. It might be different that what the original author envisioned.

For each number of consecutive months from 1 to 12 inclusive, calculate the maximum possible number of days those consecutive months could have. (Choose the starting month and year that gives highest possible answer.) Also, figure out what the earliest and lastest starting months are that can yield the maximum number of days.

For all my code below, months are numbered starting at 0. That is,

0 means January

11 means December


In [1]:
MONTHS_PER_YEAR = 12

In [2]:
# for unknown year
def max_month_length(month):
    """Return maximum number of days for given month.
    month is zero-based.
    That is,
    0 means January,
    11 means December,
    12 means January (again)
    -2 means November (yup, wraps around both ways)"""
    max_month_lengths = (
        31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
    return max_month_lengths[month % len(max_month_lengths)]

In [3]:
max_month_length(0)  # January


Out[3]:
31

In [4]:
max_month_length(13)  # February


Out[4]:
29

In [5]:
max_month_length(-2)  # November


Out[5]:
30

In [6]:
def max_days_n_months(n_months, starting_month=None):
    """Return the maximum number of days
    in n_months whole consecutive months,
    optionally starting with starting_month.
    
    If starting_month is None or is not specified,
    return highest value for all possible starting months."""
    if starting_month is not None:
        return sum(
            max_month_length(month)
            for month in range(starting_month, starting_month + n_months)
        )
    
    return max(
        max_days_n_months(n_months, starting_month)
        for starting_month in range(MONTHS_PER_YEAR)
    )

In [7]:
def foo(n):
    for n_months in range(1, n+1):
        n_days = max_days_n_months(n_months)
        yield n_months, n_days

In [8]:
n = MONTHS_PER_YEAR
# %timeit list(foo(n))
list(foo(n))


Out[8]:
[(1, 31),
 (2, 62),
 (3, 92),
 (4, 123),
 (5, 153),
 (6, 184),
 (7, 215),
 (8, 245),
 (9, 276),
 (10, 306),
 (11, 337),
 (12, 366)]

The maximum number of days in the above output matches that output by days_spanned.sh.

Now to add the stuff that keeps track of which starting months can yield those maximum number of days.


In [9]:
from collections import defaultdict

def max_n_days_for_months():
    """Yield tuples of
        number of consecutive months,
        maximum number of days for those consecutive months
        and list of starting months which produce that above maximum
    for all numbers of consecutive months up to a year."""
    for n_months in range(1, MONTHS_PER_YEAR+1):
        d = defaultdict(list)
        for starting_month in range(MONTHS_PER_YEAR):
            n_days = max_days_n_months(n_months, starting_month)
            d[n_days].append(starting_month)
        max_n_days = max(d)
        yield n_months, max_n_days, sorted(d[max_n_days])

In [10]:
# %timeit list(max_n_days_for_months())
list(max_n_days_for_months())


Out[10]:
[(1, 31, [0, 2, 4, 6, 7, 9, 11]),
 (2, 62, [6, 11]),
 (3, 92, [2, 4, 5, 6, 7, 9, 10]),
 (4, 123, [4, 6, 9]),
 (5, 153, [2, 3, 4, 5, 6, 7, 8]),
 (6, 184, [2, 4, 6, 7]),
 (7, 215, [6]),
 (8, 245, [2, 4, 5]),
 (9, 276, [4]),
 (10, 306, [2, 3]),
 (11, 337, [2]),
 (12, 366, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])]

In [11]:
def pretty():
    """Yields lines to be absolutely identical 
    to that from days_spanned.sh."""
    for selector, name in ((min, '-gt'), (max, '-ge')):
        yield f'Compare: {name} '
        for n_months, max_n_days, months in max_n_days_for_months():
            month = selector(months) + 1
            if n_months == 1:
                yield f'month: {month} month spans: {max_n_days} days  '
            elif n_months == 2:
                yield f'month: {month} plus following month spans: {max_n_days} days  '
            else:
                yield f'month: {month} plus following {n_months - 1} months spans: {max_n_days} days  '
        yield ''

In [12]:
for line in pretty():
    print(line)


Compare: -gt 
month: 1 month spans: 31 days  
month: 7 plus following month spans: 62 days  
month: 3 plus following 2 months spans: 92 days  
month: 5 plus following 3 months spans: 123 days  
month: 3 plus following 4 months spans: 153 days  
month: 3 plus following 5 months spans: 184 days  
month: 7 plus following 6 months spans: 215 days  
month: 3 plus following 7 months spans: 245 days  
month: 5 plus following 8 months spans: 276 days  
month: 3 plus following 9 months spans: 306 days  
month: 3 plus following 10 months spans: 337 days  
month: 1 plus following 11 months spans: 366 days  

Compare: -ge 
month: 12 month spans: 31 days  
month: 12 plus following month spans: 62 days  
month: 11 plus following 2 months spans: 92 days  
month: 10 plus following 3 months spans: 123 days  
month: 9 plus following 4 months spans: 153 days  
month: 8 plus following 5 months spans: 184 days  
month: 7 plus following 6 months spans: 215 days  
month: 6 plus following 7 months spans: 245 days  
month: 5 plus following 8 months spans: 276 days  
month: 4 plus following 9 months spans: 306 days  
month: 3 plus following 10 months spans: 337 days  
month: 12 plus following 11 months spans: 366 days  

The above output exactly matches the output from days_spanned.sh, but my code is ugly. The code above is not as readable as I like.

Later, I polished below.


In [13]:
known_good_output = ''.join(f'{line}\n' for line in pretty())

In [14]:
def pretty():
    """Yields lines to be absolutely identical 
    to that from days_spanned.sh."""
    for selector, name in ((min, '-gt'), (max, '-ge')):
        yield f'Compare: {name} '
        for n_months, max_n_days, months in max_n_days_for_months():
            month = selector(months) + 1
            if n_months == 1:
                duration_prose = f'month'
            elif n_months == 2:
                duration_prose = f'plus following month'
            else:
                duration_prose = f'plus following {n_months - 1} months'
            yield f'month: {month} {duration_prose} spans: {max_n_days} days  '
        yield ''

In [15]:
assert known_good_output == ''.join(f'{line}\n' for line in pretty())

Now to change the output to be more readable to me.


In [16]:
MONTH_NAMES = '''
    January February March April May June
    July August September October November December
'''.split()

MONTH_NAMES


Out[16]:
['January',
 'February',
 'March',
 'April',
 'May',
 'June',
 'July',
 'August',
 'September',
 'October',
 'November',
 'December']

In [17]:
# derived from https://stackoverflow.com/questions/38981302/converting-a-list-into-comma-separated-string-with-and-before-the-last-item

def oxford_comma_join(items, join_word='and'):
    # print(f'items={items!r} join_word={join_word!r}')
    items = list(items)
    if not items:
        return ''
    elif len(items) == 1:
        return items[0]
    elif len(items) == 2:
        return f' {join_word} '.join(items)
    else:
        return ', '.join(items[:-1]) + f', {join_word} ' + items[-1]

In [18]:
test_data = (
    # (args for oxford_comma_join, correct output),
    ((('',),), ''),
    ((('lonesome term',),), 'lonesome term'),
    ((('here', 'there'),), 'here and there'),
    ((('you', 'me', 'I'), 'or'), 'you, me, or I'),
    ((['here', 'there', 'everywhere'], 'or'), 'here, there, or everywhere'),
)

for args, known_good_output in test_data:
    # print(f'args={args!r}, k={known_good_output!r}, output={oxford_comma_join(*args)!r}')
    assert oxford_comma_join(*args) == known_good_output

In [19]:
import inflect

p = inflect.engine()

In [20]:
from textwrap import wrap

In [21]:
def pretty():
    """For number of consecutive months, up to 12,
    yields sentences that show for each number of consecutive months,
    the maximum possible number of days in those consecutive months,
    and for which starting months one can have
    those maximum possible number of days."""
    for n_months, max_n_days, months in max_n_days_for_months():
        month_names = (MONTH_NAMES[month] for month in months)
        yield (
            f'{n_months} consecutive {p.plural("month", n_months)} '
            f'can have at most {max_n_days} days '
            f'if starting in {oxford_comma_join(month_names, "or")}.'
        )

In [22]:
for sentence in pretty():
    for line in wrap(sentence):
        print(line)


1 consecutive month can have at most 31 days if starting in January,
March, May, July, August, October, or December.
2 consecutive months can have at most 62 days if starting in July or
December.
3 consecutive months can have at most 92 days if starting in March,
May, June, July, August, October, or November.
4 consecutive months can have at most 123 days if starting in May,
July, or October.
5 consecutive months can have at most 153 days if starting in March,
April, May, June, July, August, or September.
6 consecutive months can have at most 184 days if starting in March,
May, July, or August.
7 consecutive months can have at most 215 days if starting in July.
8 consecutive months can have at most 245 days if starting in March,
May, or June.
9 consecutive months can have at most 276 days if starting in May.
10 consecutive months can have at most 306 days if starting in March
or April.
11 consecutive months can have at most 337 days if starting in March.
12 consecutive months can have at most 366 days if starting in
January, February, March, April, May, June, July, August, September,
October, November, or December.

In [23]:
def not_so_pretty():
    """For number of consecutive months, up to 12,
    yields sentences that show for each number of consecutive months,
    the maximum possible number of days in those consecutive months,
    and for which starting months one can have
    those maximum possible number of days."""
    for n_months, max_n_days, months in max_n_days_for_months():
        month_names = (MONTH_NAMES[month][:3] for month in months)
        yield (
            f'{n_months} months '
            f'have max {max_n_days} days '
            f'starting in {oxford_comma_join(month_names, "or")}.'
        )

In [24]:
for sentence in not_so_pretty():
    print(sentence)


1 months have max 31 days starting in Jan, Mar, May, Jul, Aug, Oct, or Dec.
2 months have max 62 days starting in Jul or Dec.
3 months have max 92 days starting in Mar, May, Jun, Jul, Aug, Oct, or Nov.
4 months have max 123 days starting in May, Jul, or Oct.
5 months have max 153 days starting in Mar, Apr, May, Jun, Jul, Aug, or Sep.
6 months have max 184 days starting in Mar, May, Jul, or Aug.
7 months have max 215 days starting in Jul.
8 months have max 245 days starting in Mar, May, or Jun.
9 months have max 276 days starting in May.
10 months have max 306 days starting in Mar or Apr.
11 months have max 337 days starting in Mar.
12 months have max 366 days starting in Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, or Dec.