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/

Source: codewars | Difficulty: EASY
Tags: Fundamentals

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('[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         {}\nhour           {}\nday of month   {}\nmonth          {}\nday 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('{}'.format('|'.join(DAYS)), lambda x: str(DAYS.index(x.group())), ct)
	ct = re.sub('{}'.format('|'.join(MONTHS)), lambda x: str(MONTHS.index(x.group())), ct)
	ct = [re.sub('\\*', 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):
	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):
	range_list = []
	ranges = re.split('\\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('-|/', 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):
	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('[^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])