summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--perc431
-rw-r--r--perc.cfg99
2 files changed, 530 insertions, 0 deletions
diff --git a/perc b/perc
new file mode 100644
index 0000000..c906b42
--- /dev/null
+++ b/perc
@@ -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 } }
+ }
+ }
+}