diff options
-rw-r--r-- | perc | 431 | ||||
-rw-r--r-- | perc.cfg | 99 |
2 files changed, 530 insertions, 0 deletions
@@ -0,0 +1,431 @@ +#! /usr/bin/env python + +# McAuthor: CarpNet +# TODO 1: some crazy AI heuristics stuff, see The Terminator and Skynet for ideas. + +# TODO 2: Add time travel capability (note: mind the butterfly effect and for god's sake don't +# sleep with your relatives in the past we don't want anyone ending up as their own Grandad or +# Grandma. Just because your Gran looked fetching in that photo of her on the beach in the 1950's +# doesn't mean you should hit on her and end up creating a paradox that could potentially end the +# universe, fucking hell guys this is just for telling percentages of time!) + +# TODO 3: Add recognised time periods to be accepted (e.g. Friday, Tomorrow, Next Week, Next Year, New Years, Christmas) +# TODO 4: Display percentage as a chart instead +# TODO 5: Extend chart to be different types (bar, pie, raised-arm-man etc.) +# Raised arm man chart: \o/ = 1 hour, \ = 20 minutes, \o = 40 minutes etc. +# TODO 6: Web trawling important date finder + +# CHANGE LOG: +# 3.21: - Bug fix for option --NICK. Changed option back to --nick +# - Change log added +# 3.22: - Fix bug fix to fix the fix that was fixed in the last bug fix +# 3.23: - Assumes end time if given one argument, start and end if given two. +# - Restructuring of code in parseArgs function + +import datetime +import decimal +import json +import optparse +import re +import string +import sys + +PROG = "perc" +VERSION = "3.23" + +CONFIG_FILE = "perc.cfg" + +NICK = "default" +CONFIG = {} + +MONDAY = 0 +TUESDAY = 1 +WEDNESDAY = 2 +THURSDAY = 3 +FRIDAY = 4 +SATURDAY = 5 +SUNDAY = 6 + +WEEK_DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + +WEEK_LENGTH = 7 +WEEKEND = [ SATURDAY, SUNDAY ] +WORK_WEEK = [ MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY ] + +IMPORTANT_TEMPORAL_EVENTS = ["tomorrow", "friday", "new year", "newyear", "christmas", "xmas", "weekend", "midday", "tg", "intense", "lunch", "easter", "halloween"] + +OUTPUT_TYPES = ["perc", "bar", "ram", "pint"] + +parser = optparse.OptionParser("{0} <finish> [options] | {0} <start> <finish> [options]".format(PROG)) +parser.add_option("-v", "--version", action="store_true", dest="version", help="Display the version of perc") +parser.add_option("-s", "--start", action="store", dest="start", help="The start time of your particular day") +parser.add_option("-f", "--finish", action="store", dest="finish", help="The finish time of your particular day") +parser.add_option("-n", "--nick", action="store", dest="nick", help="Nickname of someone (e.g. fuckwad, l_bratch, bikeman or something)") +parser.add_option("-o", "--output", action="store", type="choice", choices=OUTPUT_TYPES, dest="output", help="Output display type. Can be a simple percentage or different types of chart. Options are %s" % ", ".join(OUTPUT_TYPES)) +parser.add_option("-m", "--mobile", action="store_true", default=False, dest="mobile", help="Indicate this should be displayed appropriately for a mobile device (i.e. smaller character display width)") + +def clamp(value, min_value, max_value): + if value < min_value: + return min_value + elif value > max_value: + return max_value + + return value + +def float_to_decimal(f): + # http://docs.python.org/library/decimal.html#decimal-faq + "Convert a floating point number to a Decimal with no loss of information" + + n, d = f.as_integer_ratio() + numerator, denominator = decimal.Decimal(n), decimal.Decimal(d) + ctx = decimal.Context(prec=60) + result = ctx.divide(numerator, denominator) + + while ctx.flags[decimal.Inexact]: + ctx.flags[decimal.Inexact] = False + ctx.prec *= 2 + result = ctx.divide(numerator, denominator) + + return result + +def delta_in_seconds(delta): + return (delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6) / 10**6 + +def calculate_ratio(start, finish): + total = float(delta_in_seconds(finish - start)) + done = float(delta_in_seconds(datetime.datetime.now() - start)) + + ratio = 0.0 + if total != 0.0: + ratio = done / total + + return ratio + +def round_to_sigfig(number, sigfig): + # http://stackoverflow.com/questions/2663612/nicely-representing-a-floating-point-number-in-python/2663623#2663623 + + assert(sigfig > 0) + + try: + d = decimal.Decimal(number) + except TypeError: + d = float_to_decimal(float(number)) + + sign , digits,exponent = d.as_tuple() + + if len(digits) < sigfig: + digits = list(digits) + digits.extend([0] * (sigfig - len(digits))) + + shift = d.adjusted() + result = int(''.join(map(str, digits[:sigfig]))) + + # Round the result + if len(digits) > sigfig and digits[sigfig] >= 5: result += 1 + result = list(str(result)) + + # Rounding can change the length of result + # If so, adjust shift + shift += len(result) - sigfig + + # reset len of result to sigfig + result = result[:sigfig] + if shift >= sigfig-1: + # Tack more zeros on the end + result += ['0'] * (shift - sigfig + 1) + elif 0 <= shift: + # Place the decimal point in between digits + result.insert(shift + 1, '.') + else: + # Tack zeros on the front + assert(shift < 0) + result = ['0.'] + ['0'] * (-shift - 1) + result + + if sign: + result.insert(0, '-') + + return ''.join(result) + +def format_time(time, standard): + formatstr = CONFIG["FORMATTING"][standard] + + period = "" + + if "{period}" in formatstr: + period = "am" if time.hour < 12 else "pm" + + if time.hour > 12: + time = time.replace(hour=time.hour - 12) + + return formatstr.format(hour=time.hour, minute=time.minute, period=period) + +def parseTime(timestr): + format = "civilian" + + for key, value in CONFIG["STANDARDS"].items(): + m = re.search(value, timestr) + + if m: + hour = int(m.group(1)) + minute = int(m.group(2)) if m.group(2) else 0 + second = 0 + + period = "" + if m.group(3): + period = m.group(3).upper() + if period == "PM" and hour < 12: + hour += 12 + + if hour < 0 or hour > 23 or minute < 0 or minute > 59 or second < 0 or second > 59: + print "Not a possible time '%s'" % timestr + sys.exit(1) + + return datetime.datetime.now().replace(hour=hour, minute=minute, second=second, microsecond=0), key + + print "Not a correctly formatted time/day: '%s'" % timestr + sys.exit(1) + +def isImportantTemporalEvent(event): + return event in IMPORTANT_TEMPORAL_EVENTS + +def isTime(timestr): + for key, value in CONFIG["STANDARDS"].items(): + + m = re.search(value, timestr) + + if m: return True + + return False + +def getDayOfWeek(day): + now = datetime.datetime.now() + + return (now - day_of_week() * datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + day * datetime.timedelta(days=1) + +def parseTemporalEvent(event): + start, finish = None, None + + now = datetime.datetime.now() + + if event == "tomorrow": + start = now.replace(hour=0, minute=0, second=0, microsecond=0) + finish = now.replace(day=now.day + 1, hour=0, minute=0, second=0, microsecond=0) + + start_formatted = WEEK_DAY_NAMES[day_of_week()] + finish_formatted = WEEK_DAY_NAMES[day_of_week(datetime.date.today() + datetime.timedelta(days=1))] + elif event in ["christmas", "xmas"]: + start = now.replace(year=now.year - 1, month=12, day=25, hour=0, minute=0, second=0, microsecond=0) + finish = now.replace(year=now.year, month=12, day=25, hour=0, minute=0, second=0, microsecond=0) + + start_formatted = "%s %d" % (string.capwords(event), start.year) + finish_formatted = "%s %d" % (string.capwords(event), finish.year) + elif event in ["new year", "newyear"]: + start = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + finish = now.replace(year=now.year + 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + + start_formatted = str(start.year) + finish_formatted = str(finish.year) + elif event == "weekend": + start = getDayOfWeek(MONDAY) + finish = getDayOfWeek(SATURDAY) + + start_formatted = "Monday" + finish_formatted = "Weekend" + elif event == "friday": + start = getDayOfWeek(MONDAY) + finish = getDayOfWeek(FRIDAY) + + start_formatted = "Monday" + finish_formatted = "Friday" + elif event == "midday": + start = now.replace(hour=0, minute=0, second=0, microsecond=0) + finish = now.replace(hour=12, minute=0, second=0, microsecond=0) + + start_formatted = "Midnight" + finish_formatted = "Midday" + elif event == "halloween": + start = now.replace(year=now.year - 1, month=10, day=31, hour=0, minute=0, second=0, microsecond=0) + finish = now.replace(year=now.year, month=10, day=31, hour=0, minute=0, second=0, microsecond=0) + + start_formatted = "Halloween %d" % start.year + finish_formatted = "Halloween %d" % finish.year + elif event == "intense": + start = now.replace(year=2011, month=9, day=4, hour=18, minute=0, second=0, microsecond=0) + finish = now.replace(year=2011, month=11, day=11, hour=20, minute=0, second=0, microsecond=0) + + start_formatted = "last intense" + finish_formatted = "next intense (minibirthday)" + elif event == "tg": + start = now.replace(year=2011, month=4, day=24, hour=10, minute=0, second=0, microsecond=0) + finish = now.replace(year=2012, month=4, day=4, hour=12, minute=0, second=0, microsecond=0) + + start_formatted = "TG %d" % start.year + finish_formatted = "TG %d" % finish.year + elif event == "lunch": + lunch_start = now.replace(hour=0, minute=0, second=0, microsecond=0).replace(**CONFIG["DEFAULTS"][NICK]["lunch"]["start"]) + lunch_finish = now.replace(hour=0, minute=0, second=0, microsecond=0).replace(**CONFIG["DEFAULTS"][NICK]["lunch"]["finish"]) + + if now < lunch_start: + start = now.replace(hour=0, minute=0, second=0, microsecond=0).replace(**CONFIG["DEFAULTS"][NICK]["start"]) + finish = lunch_start + + start_formatted = format_time(start, CONFIG["DEFAULTS"][NICK]["standard"]) + finish_formatted = "lunch (%s)" % format_time(finish, CONFIG["DEFAULTS"][NICK]["standard"]) + else: + start = lunch_start + finish = lunch_finish + + start_formatted = format_time(start, CONFIG["DEFAULTS"][NICK]["standard"]) + finish_formatted = format_time(finish, CONFIG["DEFAULTS"][NICK]["standard"]) + + return start, finish, start_formatted, finish_formatted + +def readConfig(): + global CONFIG + with open(CONFIG_FILE, "r") as f: + CONFIG = json.load(f) + + if not CONFIG: + print "Config file '%s' not found. Exiting..." % CONFIG_FILE + sys.exit(1) + + if "STANDARDS" not in CONFIG: + print "No STANDARDS specified in config file: '%s'" % CONFIG_FILE + sys.exit(1) + + if "FORMATTING" not in CONFIG: + print "No FORMATTING options specified in config file: '%s'" % CONFIG_FILE + sys.exit(1) + + # TODO: can do set comparison here (possibly disjoint?) + for key in CONFIG["STANDARDS"]: + if key not in CONFIG["FORMATTING"]: + print "No format specified for standard '%s'" % key + sys.exit(1) + +def day_of_week(day=datetime.date.today()): + return datetime.date.weekday(day) + +def isWeekend(): + return day_of_week() in WEEKEND + +def parseArgs(args): + global NICK + NICK = args[0].lower() if args else "default" + if options.nick: + NICK = options.nick.lower() + if NICK not in CONFIG["DEFAULTS"]: + NICK = "default" + + start, finish = None, None + start_formatted, finish_formatted = None, None + now = datetime.datetime.now() + + if len(args) >= 2 and isImportantTemporalEvent(args[1].lower()): + start, finish, start_formatted, finish_formatted = parseTemporalEvent(args[1].lower()) + elif len(args) >= 3: + start, standard = parseTime(args[1]) + if options.start: + start, standard = parseTime(options.start) + + finish, standard = parseTime(args[2]) + if options.finish: + finish, standard = parseTime(options.finish) + + if finish < start: + finish = finish.replace(day=finish.day + 1) + + start_formatted = format_time(start, standard) + finish_formatted = format_time(finish, standard) + elif len(args) >= 2: + start = now.replace(hour=0, minute=0, second=0, microsecond=0).replace(**CONFIG["DEFAULTS"][NICK]["start"]) + + finish, standard = parseTime(args[1]) + if options.finish: + finish, standard = parseTime(options.finish) + + if finish < start: + finish = finish.replace(day=finish.day + 1) + + start_formatted = format_time(start, standard) + finish_formatted = format_time(finish, standard) + else: + standard = None + + start = now.replace(hour=0, minute=0, second=0, microsecond=0).replace(**CONFIG["DEFAULTS"][NICK]["start"]) + if options.start: + start, standard = parseTime(options.start) + + finish = now.replace(hour=0, minute=0, second=0, microsecond=0).replace(**CONFIG["DEFAULTS"][NICK]["finish"]) + if options.finish: + finish, standard = parseTime(options.finish) + + if standard is None: + standard = CONFIG["DEFAULTS"][NICK]["standard"] + + if finish < start: + finish = finish.replace(day=finish.day + 1) + + start_formatted = format_time(start, standard) + finish_formatted = format_time(finish, standard) + + output_type = options.output if options.output else CONFIG["DEFAULTS"][NICK]["output"] + + ratio = clamp(calculate_ratio(start, finish), 0.0, 1.0) + + return start_formatted, finish_formatted, ratio, output_type + +class Output(object): + + def perc(self, ratio, start, finish): + print CONFIG["FORMATTING"]["PERCENTAGE"].format(ratio=ratio, start=start, finish=finish) + + def bar(self, ratio, start, finish): + length = CONFIG["FORMATTING"]["BAR"]["length"] if not options.mobile else CONFIG["FORMATTING"]["BAR"]["length_mobile"] + char = CONFIG["FORMATTING"]["BAR"]["character"] + + current = clamp(int(ratio * length), 0, length) + + space = " " * (length - current) + if space: space = ">" + space[1:] + + ratiostr = "{0}%".format(round_to_sigfig(ratio * 100, CONFIG["RATIO_SIGFIGS"])) if current >= 7 else "" + + print "{start} [{ratio:{char}^{current}}{space}] {finish}".format(ratio=ratiostr, char=char, current=current, space=space, start=start, finish=finish) + + def ram(self, ratio, start, finish): + # 10 raised arm men indicates a complete day + + # Raised arm men parts (3 parts per raised arm man and 10 of them) + ram_parts = 1.0 / (3.0 * 10.0) + + def pint(self, ratio, start, finish): + pass + + def output(self, type, ratio, start, finish): + func = getattr(self, type, self.perc) + + func(ratio, start, finish) + +if __name__ == "__main__": + readConfig() + + global options + + (options, args) = parser.parse_args() + + if options.version: + print "v%s" % VERSION + sys.exit(0) + + if isWeekend(): + print "It's the freekin' Weekend!!!!" + sys.exit(0) + + if "perc" in args: + args.remove("perc") + + start, finish, ratio, output_type = parseArgs(args) + + output = Output() + output.output(output_type, ratio, start, finish) diff --git a/perc.cfg b/perc.cfg new file mode 100644 index 0000000..a3f02b6 --- /dev/null +++ b/perc.cfg @@ -0,0 +1,99 @@ +{ + "RATIO_SIGFIGS": 3, + + "STANDARDS": { + "civilian": "^(\\d+):(\\d{2})()$", + "military": "(\\d+)(\\d{2})()$", + "12hour": "^(\\d+):(\\d{2})([a-zA-Z]{2})$", + "12houronly": "^(\\d+)()([a-zA-Z]{2})$", + "24houronly": "^(\\d+)()()$" + }, + + "FORMATTING": { + "civilian": "{hour:02d}:{minute:02d}", + "military": "{hour:02d}{minute:02d}", + "12hour": "{hour:02d}:{minute:02d}{period}", + "12houronly": "{hour}{period}", + "24houronly": "{hour}", + + "PERCENTAGE": "{ratio:.2%} {finish} ({start} start)", + "BAR": { + "character": "-", + "length": 40, + "length_mobile": 30 + } + }, + + "DEFAULTS": { + "default": { + "start": { "hour": 9 }, + "finish": { "hour": 17 }, + "standard": "civilian", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "carpnet": { + "start": { "hour": 10 }, + "finish": { "hour": 19 }, + "standard": "12houronly", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "l_bratch": { + "start": { "hour": 8, "minute": 30 }, + "finish": { "hour": 17 }, + "standard": "civilian", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "bikeman": { + "start": { "hour": 9 }, + "finish": { "hour": 17 }, + "standard": "12houronly", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "otherlw": { + "start": { "hour": 10 }, + "finish": { "hour": 19 }, + "standard": "civilian", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "wjoe": { + "start": { "hour": 9 }, + "finish": { "hour": 17 }, + "standard": "12houronly", + "output": "bar", + "lunch": { "start": { "hour": 12 }, "finish": { "hour": 13 } } + }, + "jagw": { + "start": { "hour": 9 }, + "finish": { "hour": 18 }, + "standard": "12houronly", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "miniwork": { + "start": { "hour": 9 }, + "finish": { "hour": 17, "minute": 30 }, + "standard": "civilian", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "bottlecap": { + "start": { "hour": 9 }, + "finish": { "hour": 17, "minute": 30 }, + "standard": "civilian", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + }, + "sbeans": { + "start": { "hour": 9 }, + "finish": { "hour": 17, "minute": 30 }, + "standard": "civilian", + "output": "bar", + "lunch": { "start": { "hour": 13 }, "finish": { "hour": 14 } } + } + } +} |