Time to build a crontab parser... https://en.wikipedia.org/wiki/Cron

A crontab command is made up of 5 fields in a space separated string:

```
minute: 0-59
hour: 0-23
day of month: 1-31
month: 1-12
day of week: 0-6 (0 == Sunday)
```

Each field can be a combination of the following values:

* a wildcard `*` which equates to all valid values.
* a range `1-10` which equates to all valid values between start-end inclusive.
* a step `*/2` which equates to every second value starting at 0, or 1 for day of month and month.
* a single value `2` which would be 2.
* a combination of the above separated by a `,`

Note: In the case of multiple values in a single field, the resulting values should be sorted.

Note: Steps and ranges can also be combined `1-10/2` would be every second value in the `1-10` range `1 3 5 7 9`.

For an added challenge day of week and month will both support shortened iso formats:

```
SUN MON TUE ...
JAN FEB MAR ...
```

Examples
========

`*` should output every valid value `0 1 2 3 4 5 6 ...`

`1` should output the value `1`.

`1,20,31` should output the values `1 20 31`.

`1-10` should output the values `1 2 3 4 5 6 7 8 9 10`.

`*/2` should output every second value `0 2 4 6 8 10 ...`.

`*/2` should output `1 3 5 7 9 ...` in the cases, day of month and month.

`1-10/2` should output `1 3 5 7 9`.

`*/15,3-5` should output the values `0 3 4 5 15 30 45` when applied to the minute field.

Output
======

The purpose of this kata is to take in a crontab command, and render it in a nice human readable format.

The output format should be in the format `field_name      values`. The field name should be left justified and
padded to 15 characters. The values should be a space separated string of the returned values.

For example the crontab `*/15 0 1,15 * 1-5` would have the following output:

```
minute          0 15 30 45
hour            0
day of month    1 15
month           1 2 3 4 5 6 7 8 9 10 11 12
day of week     1 2 3 4 5
```

All test cases will receive a valid crontab, there is no need to handle an incorrect number of values.

Super handy resource for playing with different values to find their outputs: https://crontab.guru/

Difficulty: introductory

Starter code:

def parse(crontab):
	

Solutions:
RANGES = {
    'minute': (0, 59),
    'hour': (0, 23),
    'day of month': (1, 31),
    'month': (1, 12),
    'day of week': (0, 6),
}

ALIASES = {
    'month': ' JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC'.split(' '),
    'day of week': 'SUN MON TUE WED THU FRI SAT'.split(),
}

def get_alias(field, value):
    try:
        return ALIASES[field].index(value)
    except:
        return int(value)

def parse(crontab):
    def parse_field(field, value):
        values = set()
        for part in value.split(','):
            part, *end = part.split('/')
            step = int(end[0]) if end else 1
            if part == '*':
                start, stop = RANGES[field]
            else:
                part, *end = part.split('-')
                start = get_alias(field, part)
                stop = get_alias(field, end[0]) if end else start
            values.update(range(start, stop + 1, step))
        return ' '.join(map(str, sorted(values)))
    return '\n'.join(f'{field:<15}{parse_field(field, value)}'
                     for field, value in zip(RANGES, crontab.split()))

from itertools import chain
import re

MONTHS_DAYS = list(map(str.split, ('JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC', 'SUN MON TUE WED THU FRI SAT')))
REPL_NAMES  = {s: str(i) for i,s in chain(*( enumerate(lst,detla) for lst,detla in zip(MONTHS_DAYS, (1,0))))}
RANGES      = (('minute',0,59), ('hour',0,23), ('day of month',1,31), ('month',1,12), ('day of week',0,6))

def parse(s):
    s = re.sub(r'[A-Z]+', lambda m: REPL_NAMES[m[0]], s)
    return '\n'.join( parsePart(part,name,(a,b)) for part,(name,a,b) in zip(s.split(), RANGES))

def parsePart(part, name, rng):
    out = [] 
    for s in part.split(','):
        i,j       = list(map(s.find,'-/'))
        step,j    = (1,len(s)) if j==-1 else (int(s[j+1:]),j)
        start,end = rng if s[0]=='*' else (int(s),)*2 if i==-1 else list(map(int, (s[:i],s[i+1:j])))
        out.extend(list(range(start, end+1, step)))
        
    return f"{name:15}{ ' '.join(map(str, sorted(out))) }"


import re

D = {0:[0,59],1:[0,23],2:[1,31],3:[1,12],4:[0,6]}
REGEXES = ['(\d+)-(\d+)','(\d+)-(\d+)/(\d+)','(\d+)']
FORMAT = '''\
minute         {}
hour           {}
day of month   {}
month          {}
day of week    {}\
'''
DAYS = 'SUN MON TUE WED THU FRI SAT'.split()
MONTHS = 'X JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DES'.split()

def parse(ct):
        ct = re.sub(r'{}'.format('|'.join(DAYS)),lambda x:str(DAYS.index(x.group())), ct)
        ct = re.sub(r'{}'.format('|'.join(MONTHS)),lambda x:str(MONTHS.index(x.group())),ct)
        ct = [re.sub(r'\*',lambda _:'-'.join([str(k) for k in D[i]]),j) for i,j in enumerate(ct.split())]
        parser = []
        for i,j in enumerate(ct):
              sep = []
              for k in j.split(','):
                  a,b,c = [re.search('^'+r+'$',k) for r in '(\d+)-(\d+) (\d+)-(\d+)/(\d+) (\d+)'.split()]
                  if c : sep.append(int(k))
                  elif b:
                      x,y,z = map(int,b.groups())
                      sep.extend(list(range(x,y+1,z)))
                  elif a:
                      x,y = map(int,a.groups())
                      sep.extend(list(range(x,y+1)))
              parser.append(' '.join([str(i) for i in sorted(sep)]))
        return FORMAT.format(*parser)

import re

WILDCARDS = {
    'minute': {
        'range': list(range(60))
    },
    'hour': {
        'range': list(range(24))
    },
    'day-of-month': {
        'range': list(range(1, 32))
    },
    'month': {
        'range': list(range(1, 13)),
        'names': {
            'JAN': 1,
            'FEB': 2,
            'MAR': 3,
            'APR': 4,
            'MAY': 5,
            'JUN': 6,
            'JUL': 7,
            'AUG': 8,
            'SEP': 9,
            'OCT': 10,
            'NOV': 11,
            'DEC': 12
        }
    },
    'day-of-week': {
        'range': list(range(7)),
        'names': {'MON': 1, 'TUE': 2, 'WED': 3, 'THU': 4, 'FRI': 5, 'SAT': 6, 'SUN': 0}
    }
}

def parse_interval(interval, type):
    rng = WILDCARDS[type]['range']
    substitutes = WILDCARDS[type].get('names', {})

    for k, v in substitutes.items():
        interval = interval.replace(k, str(v))

    interval = interval.replace('*', '{}-{}'.format(rng[0], rng[-1]))
    return range_parser(interval)

def parse(crontab):
    (minute, hour, day_month, month, day_week) = crontab.split(' ')

    out = [
        ['minute', parse_interval(minute, 'minute')], 
        ['hour', parse_interval(hour, 'hour')], 
        ['day of month', parse_interval(day_month, 'day-of-month')], 
        ['month', parse_interval(month, 'month')], 
        ['day of week', parse_interval(day_week, 'day-of-week')]
    ]

    return format(out)


def format(cron_conf):
    """Formats results"""
    out = []
    for item in cron_conf:
        out += ["{}{}".format(str.ljust(item[0], 15), ' '.join(map(str, item[1])))]
    return '\n'.join(out)

def range_parser(range_spec):
    """Expands the range range_spec into list"""
    range_list = []
    ranges = re.split(r'\s?,\s?', range_spec)

    for rng in ranges:
        out = parse_range(rng)
        range_list += out

    return sorted(range_list)

def parse_range(rng):
    spec = re.split(r'-|/', rng)
    spec = [int(s) for s in spec]

    if len(spec) == 1:
        return [spec[0]]
    elif len(spec) == 2:
        return list(range(spec[0], spec[1] + 1))
    elif len(spec) == 3:
        return list(range(spec[0], spec[1] + 1, spec[2]))

def parse(crontab):
    """
            "minute         1",
            "hour           2",
            "day of month   3",
            "month          4",
            "day of week    5",
    """
    
    fields = crontab.split()
    
    minutes = interpret_minutes(fields[0])
    hours = interpret_hours(fields[1])
    days_m = interpret_days_month(fields[2])
    months = interpret_months(fields[3])
    days_w = interpret_days_week(fields[4])
    
    result  = "minute         " + " ".join([str(v) for v in sorted(list(minutes))]) + "\n"
    result += "hour           " + " ".join([str(v) for v in sorted(list(hours))]) + "\n"
    result += "day of month   " + " ".join([str(v) for v in sorted(list(days_m))]) + "\n"
    result += "month          " + " ".join([str(v) for v in sorted(list(months))]) + "\n"
    result += "day of week    " + " ".join([str(v) for v in sorted(list(days_w))])
    
    return result
    
def interpret_minutes(cr_minutes):

    def interpret_minute(cr_field):            
        step = "1"   
        value = cr_field
        if cr_field.find('/') != -1:
            (value, step) = cr_field.split("/")
            
        low_min = 0
        hig_min = 59
        if value == "*":
            low_min = 0
            hig_min = 59
        elif value.find('-') != -1:
            (lm, hm) = value.split("-")
            low_min = int(lm)
            hig_min = int(hm)
        else:
            low_min = hig_min = int(value)
            
        return set(range(low_min, hig_min+1, int(step)))
        
    result_set = set()
    for request in cr_minutes.split(","):
        new_set = interpret_minute(request)
        result_set.update(new_set)
        
    return result_set   
        

def interpret_hours(cr_hours):
    def interpret_hour(cr_field):            
        step = "1"   
        value = cr_field
        if cr_field.find('/') != -1:
            (value, step) = cr_field.split("/")
            
        low_min = 0
        hig_min = 23
        if value == "*":
            low_min = 0
            hig_min = 23
        elif value.find('-') != -1:
            (lm, hm) = value.split("-")
            low_min = int(lm)
            hig_min = int(hm)
        else:
            low_min = hig_min = int(value)
            
        return set(range(low_min, hig_min+1, int(step)))
    
    result_set = set()
    for request in cr_hours.split(","):
        new_set = interpret_hour(request)
        result_set.update(new_set)
        
    return result_set   


def interpret_days_month(cr_days_m):
    def interpret_day_month(cr_field):            
        step = "1"   
        value = cr_field
        if cr_field.find('/') != -1:
            (value, step) = cr_field.split("/")
            
        low_min = 1
        hig_min = 31
        if value == "*":
            low_min = 1
            hig_min = 31
        elif value.find('-') != -1:
            (lm, hm) = value.split("-")
            low_min = int(lm)
            hig_min = int(hm)
        else:
            low_min = hig_min = int(value)
            
        return set(range(low_min, hig_min+1, int(step)))
    
    result_set = set()
    for request in cr_days_m.split(","):
        new_set = interpret_day_month(request)
        result_set.update(new_set)
        
    return result_set   

    
def interpret_months(cr_months):
    def interpret_month(cr_field):
        names_to_numbers = {
            'JAN': "1", 'FEB': "2", 'MAR': "3", 'APR': "4", 'MAY': "5",
            'JUN': "6", 'JUL': "7", 'AUG': "8", 'SEP': "9", 'OCT': "10",
            'NOV': "11", 'DEC': "12"
        }
        
        step = "1"   
        value = cr_field
        if cr_field.find('/') != -1:
            (value, step) = cr_field.split("/")
            
        low_min = 1
        hig_min = 12
        if value == "*":
            low_min = 1
            hig_min = 12
        elif value.find('-') != -1:
            (lm, hm) = value.split("-")
            low_min = int(names_to_numbers.get(lm, lm))
            hig_min = int(names_to_numbers.get(hm, hm))
        else:
            low_min = hig_min = int(names_to_numbers.get(value, value))
            
        return set(range(low_min, hig_min+1, int(step)))
    
    result_set = set()
    for request in cr_months.split(","):
        new_set = interpret_month(request)
        result_set.update(new_set)
        
    return result_set   

def interpret_days_week(cr_days_w):
    def interpret_day_week(cr_field):
        names_to_numbers = {
            'SUN': "0", 'MON': "1", 'TUE': "2", 'WED': "3",
            'THU': "4", 'FRI': "5", 'SAT': "6"
        }
        
        step = "1"   
        value = cr_field
        if cr_field.find('/') != -1:
            (value, step) = cr_field.split("/")
            
        low_min = 0
        hig_min = 6
        if value == "*":
            low_min = 0
            hig_min = 6
        elif value.find('-') != -1:
            (lm, hm) = value.split("-")
            low_min = int(names_to_numbers.get(lm, lm))
            hig_min = int(names_to_numbers.get(hm, hm))
        else:
            low_min = hig_min = int(names_to_numbers.get(value, value))
            
        return set(range(low_min, hig_min+1, int(step)))
    
    result_set = set()
    for request in cr_days_w.split(","):
        new_set = interpret_day_week(request)
        result_set.update(new_set)
        
    return result_set   


from re import sub

def parse(crontab):
    
    ranges = {
        0: ['minute', list(range(60))], 
        1: ['hour', list(range(24))], 
        2: ['day of month', list(range(1, 32))], 
        3: ['month', list(range(1, 13))], 
        4: ['day of week', list(range(7))]
    }
    trans = {
        3: '# JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC'.split(' '), 
        4: 'SUN MON TUE WED THU FRI SAT SUN'.split(' ')
    }
    
    tabs = crontab.split(' ')
    res = []
    
    for i, tab in enumerate(tabs): 
        
        lb, lst = ranges[i]
        tokens = tab.split(',')
        
        local = []
        for s in tokens: 
            if sub(r'[^A-Z]', '', s):
                t_arr = trans[i]
                for t in t_arr:
                    if t in s: s = s.replace(t, str(t_arr.index(t)))
                    
            local += choose(lst, s)
        
        res.append('{}{}'.format(lb.ljust(15), ' '.join(str(v) for v in sorted(local))))
    
    return '\n'.join(res)

def choose(lst, s): 
    res = []
    
    if s == '*': return lst
    if s.isdigit(): return [i for i in lst if int(s) == i]
    
    if '-' in s and '/' not in s: 
        s, e = [int(v) for v in s.split('-')]
        return [v for v in lst if s <= v <= e]
    
    if '/' in s: 
        cr, step = s.split('/')        
        step = int(step)
        return choose(lst, cr)[0::step]        
    
    return []

D = {'JAN':1, 'FEB':2, 'MAR':3, 'APR':4, 'MAY':5, 'JUN':6, 'JUL':7, 'AUG':8, 'SEP':9, 'OCT':10, 'NOV':11, 'DEC':12,
     'SUN':0, 'MON':1, 'TUE':2, 'WED':3, 'THU':4, 'FRI':5, 'SAT':6}

def read(s, mini, maxi):
    res = set()
    for x in s.split(','):
        y = x.split('/')
        a, n = y if len(y) == 2 else (x, '1')
        z = a.split('-')
        b, c = z if len(z) == 2 else (mini, maxi) if a == '*' else (a, a)
        res.update(range(D[b] if b in D else int(b), (D[c] if c in D else int(c))+1, int(n)))
    return ' '.join(map(str, sorted(res)))

def parse(crontab):
    res, L = [], crontab.split()
    res.append(f"minute         {read(L[0], 0, 59)}")
    res.append(f"hour           {read(L[1], 0, 23)}")
    res.append(f"day of month   {read(L[2], 1, 31)}")
    res.append(f"month          {read(L[3], 1, 12)}")
    res.append(f"day of week    {read(L[4], 0, 6)}")
    return '\n'.join(res)

def parse(crontab):
    crontab = crontab.replace('FEB', '2').replace('JUL', '7').replace('SUN', '0').replace('THU', '4').split()
    output = [[], [], [], [], []]
    valid = {0: [0, 59], 1: [0, 23], 2: [1, 31], 3: [1, 12], 4: [0, 6]}
    def parser(s):
        nums = '1234567890'
        out = []
        dig1 = dig2 = dig3 = ''
        state = 0
        for c in s:
            if c == ',':
                if not(dig3):
                    dig3 = '1'
                out.append([dig1, dig2, dig3])
                dig1 = dig2 = dig3 = ''
                state = 0
            elif not(state):
                dig1 += c
                state = 1
            elif state == 1:
                if c in nums:
                    dig1 += c
                elif c == '-':
                    state = 2
                else:
                    state = 3
            elif state == 2:
                if c in nums:
                    dig2 += c
                else:
                    state = 3
            elif state == 3:
                dig3 += c
        if not(dig3):
            dig3 = '1'
        out.append([dig1, dig2, dig3])
        return out
    for i in range(len(crontab)):
        for p in parser(crontab[i]):
            if p[0] == '*':
                output[i].extend([x for x in range(valid[i][0], valid[i][1] + 1, int(p[2]))])
            elif p[1]:
                output[i].extend([x for x in range(int(p[0]), int(p[1]) + 1, int(p[2])) if valid[i][0] <= x <= valid[i][1]])
            else:
                output[i].append(int(p[0]))
        output[i].sort()
        output[i] = ' '.join([str(x) for x in output[i]])
    return 'minute         ' + output[0] + '\nhour           ' + output[1] + '\nday of month   ' + output[2] + '\nmonth          ' + output[3] + '\nday of week    ' + output[4]


def parse(crontab):
    cron_list = crontab.split(' ')
    cron_ranges = ['0-59', '0-23', '1-31', '1-12', '0-6']
    weekdays = {'SUN': 0, 'MON': 1, 'TUE': 2, 'WED': 3, 'THU': 4, 'FRI': 5, 'SAT': 6}
    months = {'JAN': 1, 'FEB': 2, 'MAR': 3, 'APR': 4, 'MAY': 5, 'JUN': 6, 'JUL': 7, 'AUG': 8, 'SEP': 9, 'OCT': 10, 'NOV': 11, 'DEC': 12}

    for i, r in enumerate(cron_list):
        val_out = []
        for v in r.split(','):
            step = 1
            if '/' in v:
                step = int(v.split('/')[1])
                v = v.split('/')[0]
            if '-' in v:
                start = (months[v.split('-')[0]] if i == 3 else weekdays[v.split('-')[0]]) if not v.split('-')[0].isnumeric() else int(v.split('-')[0])
                stop = (months[v.split('-')[1]] + 1 if i == 3 else weekdays[v.split('-')[1]] + 1) if not v.split('-')[1].isnumeric() else int(v.split('-')[1]) + 1
                val_out += [i for i in range(start, stop, step)]
            elif '*' in v:
                start = int(cron_ranges[i].split('-')[0])
                stop = int(cron_ranges[i].split('-')[1]) + 1
                val_out += [i for i in range(start, stop, step)]
            else:
                val_out += [int(v)]
        cron_list[i] = ' '.join([str(i) for i in sorted(val_out)])

    return '{:<15}{}\n{:<15}{}\n{:<15}{}\n{:<15}{}\n{:<15}{}'.format('minute', cron_list[0], 'hour', cron_list[1],
                                                                     'day of month', cron_list[2], 'month', cron_list[3],
                                                                     'day of week', cron_list[4])

isodays=['SUN','MON','TUE','WED','THU','FRI','SAT']
isomonths=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']
def checkIso(start,end):
    if start in isodays:
        return str(isodays.index(start)),str(isodays.index(end))
    if start in isomonths:
        return str(isomonths.index(start)+1),str(isomonths.index(end)+1)
    return start,end

def parseitem(itempart):
    itemnums=list()
    period,item,minmax = itempart
    for i in item.split(','):
    
        if i.isnumeric():
            itemnums.append(int(i))
            continue

        if i.find('-')>-1:
            rangsplit=i.split('-') 
            step=1
            start=rangsplit[0]
            end=rangsplit[1]
            if rangsplit[1].find('/')>-1:
                step=int(rangsplit[1].split('/')[1])         
                end=rangsplit[1].split('/')[0]
            start,end=checkIso(start,end)
            itemnums.extend(range(int(start),int(end)+1,step))
            continue
        if i.find('*')>-1:
            step=1
            if i.find('/')>-1:
                step=int(i.split('/')[1])
            itemnums.extend(range(minmax[0],minmax[1]+1,step))
            continue

    itemnums.sort()    
    return period.ljust(15)+' '.join([ str(x) for x in itemnums ])



def parse(crontab):
    periods=['minute','hour','day of month','month','day of week']
    periodminmax=[[0,59],[0,23],[1,31],[1,12],[0,6]]
    zipped=list(zip(periods,crontab.split(' '),periodminmax))
    return '\n'.join([ parseitem(x) for x in zipped ])