#!/usr/bin/python """Given a list of files, draw a compact visual calendar representation of the dates those files were created. Contains one useful library function, dategraph(), that works on a list of Unix timestamps. This code was hacked out quickly as an experiment in test-driven development with no design or refactoring. It works, but it ain't pretty. By Nelson Minar 2004-04-10 """ # Configurable options to change graph output squareSize = 11 squareMargin = 2 graphMargin = 4 backgroundColor = (0x50, 0x50, 0x50) noneColor = (0x00, 0x00, 0x00) def dayColor(n): c = min(0xff, 0xb0 + (n * 0x10)) return (c, c, c) import path, time, unittest, datetime, Image, ImageDraw, sys def dategraph(filename, timeList): "Given a list of Unix timestamps, create a graph at the given filename" tally = tallyTimes(timeList) im = graphicTally(tally) im.save(filename) def main(dir): times = getFileDates(dir) dategraph('tally.png', times) def getFileDates(dir): "Get the timestamps for the files underneath the specified directory" return [f.getmtime() for f in path.path(dir).walkfiles('*.txt')] def sortuniq(seq): "Return a copy of the input sequence sorted and uniqued" d = {} for s in seq: d[s] = 1 r = d.keys() r.sort() return r def ymd(t): "Return the local year, month, day of a given timestamp (as a string)" lt = time.localtime(t) return "%4d%02d%02d" % (lt[0], lt[1], lt[2]) def ym(t): "Return the year and month of a given timestamp" return ymd(t)[:6] def day(t): "Return the day that a given timestamp represents (as a int)" return int(ymd(t)[6:]) def monthRange(minTime, maxTime): "Return a list of months between mintime and maxtime" return sortuniq([ym(t) for t in range(minTime, maxTime+86400, 86400)]) def tallyTimes(inputTimes): """Given a list of times, build a datastructure summarizing the list. Data format: { month : { day : count } } All months in the range are filled in to at least an empty dict. All day keys are not set.""" if len(inputTimes) < 1: return {} times = list(inputTimes) times.sort() minTime = times[0] maxTime = times[-1] r = {} for m in monthRange(minTime, maxTime): r[m] = {} for t in times: monthBin = r[ym(t)] monthBin[day(t)] = monthBin.get(day(t), 0) + 1 return r def daysInMonth(month): "Return a list of all the days in the given month" r = range(1, 29) # All months in modern times have at least 28 days # Now try to figure out how many more days there are y = int(month[:4]) m = int(month[4:]) dt = datetime.date(y, m, 28) oneDay = datetime.timedelta(1) dt += oneDay newDay = 29 while dt.day != 1: r.append(newDay) newDay += 1 dt += oneDay return r def textTally(tally): """Turn a tallyTimes datastructure into a two dimensional string""" months = tally.keys() months.sort() outputList = [] for month in months: outputRow = [] for day in daysInMonth(month): count = tally[month].get(day, 0) if count == 0: outputRow.append('.') elif count < 10: outputRow.append(str(count)) else: outputRow.append('*') outputList.append(''.join(outputRow)) return '\n'.join(outputList) + '\n' def rectFor(x, y): "Return a rectangle in the image for the logical x, y coordinates" return ((x * squareSize + squareMargin, y * squareSize + squareMargin), ((x + 1) * squareSize - squareMargin - 1, (y + 1) * squareSize - squareMargin - 1)) def graphicTally(tally): """Turn a tally into a graphical block. Works by converting the text representation into graphics. What a hack!""" width = 31 height = len(tally.keys()) im = Image.new("RGB", (width*squareSize, height*squareSize), backgroundColor) draw = ImageDraw.Draw(im) x = 0 y = 0 for c in textTally(tally): if c == '\n': y += 1 x = 0 else: if c == '.': draw.rectangle(rectFor(x, y), fill=noneColor) else: draw.rectangle(rectFor(x, y), fill=dayColor(int(c))) x += 1 im2 = Image.new("RGB", (im.size[0] + graphMargin * 2, im.size[1] + graphMargin * 2), backgroundColor) im2.paste(im, (graphMargin, graphMargin)) return im2 class Tests(unittest.TestCase): def testSortuniq(self): self.assertEqual([], sortuniq([])) self.assertEqual(["foo"], sortuniq(["foo"])) self.assertEqual(["bar", "foo"], sortuniq(["foo", "bar"])) self.assertEqual(["bar", "foo"], sortuniq(["bar", "foo"])) self.assertEqual(["foo"], sortuniq(["foo"])) def testYMD(self): self.assertEqual("20040404", ymd(1081132244)) def testYM(self): self.assertEqual("200404", ym(1081132244)) def testDay(self): self.assertEqual(4, day(1081132244)) def testMonthRange(self): self.assertEqual(["196912"], monthRange(0, 0)) self.assertEqual(["200110"], monthRange(1003734000, 1003820400)) self.assertEqual(["200110", "200111"], monthRange(1004515200, 1004688000)) self.assertEqual(["200110", "200111"], monthRange(1003734000, 1004601600)) self.assertEqual(["200110", "200111"], monthRange(1004515200, 1004601600)) self.assertEqual(13, len(monthRange(1004515200, 1004515200 + 364 * 86400))) # Some test data: # 1003734000 is 2003-10-22 # 1004515200 is 2003-10-31 # 1004601600 is 2003-11-01 # 1081132244 is 2004-04-04 def testTallyTimes(self): self.assertEqual({}, tallyTimes([])) self.assertEqual({"200110": {22: 1}}, tallyTimes([1003734000])) self.assertEqual({"200110": {22: 2}}, tallyTimes([1003734000, 1003734001])) self.assertEqual({"200110": {22: 1}, "200111": {1: 1}}, tallyTimes([1003734000, 1004601600])) k = tallyTimes([1004515200, 1004515200 + 364 * 86400]).keys() self.assertEqual(13, len(k)) def testTextTally(self): self.assertEqual("\n", textTally(tallyTimes([]))) halloween = tallyTimes([1004515200]) self.assertEqual("." * 30 + '1\n', textTally(halloween)) twoOct = tallyTimes([1004515200, 1003734000]) self.assertEqual("." * 21 + '1........1\n', textTally(twoOct)) octNov = tallyTimes([1004601600, 1004515200]) self.assertEqual("." * 30 + '1\n' + "1" + 29 * '.' + '\n', textTally(octNov)) def testDaysInMonth(self): self.assertEqual(range(1, 30), daysInMonth("200402")) self.assertEqual(range(1, 32), daysInMonth("200403")) self.assertEqual(range(1, 31), daysInMonth("200404")) def testGraphicTally(self): octNov = tallyTimes([1004601600, 1004515200]) im = graphicTally(octNov) self.assertEqual((31*squareSize + graphMargin * 2, 2*squareSize + graphMargin * 2), im.size) def testRectFor(self): self.assertEqual(((squareMargin, squareMargin), (squareSize - squareMargin - 1, squareSize - squareMargin - 1)), rectFor(0, 0)) main('/home/nelson/blosxom/entries') unittest.main()