Scripting your scripting

A couple of days ago, Rob Trew tweeted a link to a shell script he wrote that prints out an emoji clock face based on the time it’s given.

🕟.sh
Simple bash .sh gets emoji clockface (stdout|clipboard) with specified time (to nearest preceding half hour)

github.com/RobTrew/txtque…

ComplexPoint (@ComplexPoint) Jul 22 2014 10:31 AM

It sounded like a fun little programming project and possibly even useful.1 Because I wanted to round to the nearest half hour instead of the nearest preceding half hour, and because my shell scripting is abysmal, I decided to rewrite it in Python.

I knew immediately that the key feature of my script would be a dictionary that mapped a time written as text, like “8:30,” to the corresponding emoji, 🕣. The rest of the script would just be input, output, and rounding. Here’s the script:

python:
 1:  #!/usr/bin/python
 2:  # -*- coding: utf-8 -*-
 3:  
 4:  from sys import argv
 5:  from time import strftime
 6:  
 7:  clocks = {'12:00': '🕛', '12:30': '🕧',  '1:00': '🕐',  '1:30': '🕜',
 8:             '2:00': '🕑',  '2:30': '🕝',  '3:00': '🕒',  '3:30': '🕞',
 9:             '4:00': '🕓',  '4:30': '🕟',  '5:00': '🕔',  '5:30': '🕠',
10:             '6:00': '🕕',  '6:30': '🕡',  '7:00': '🕖',  '7:30': '🕢',
11:             '8:00': '🕗',  '8:30': '🕣',  '9:00': '🕘',  '9:30': '🕤',
12:            '10:00': '🕙', '10:30': '🕥', '11:00': '🕚', '11:30': '🕦'}
13:  
14:  # Get the argument, which should be a time string, like "8:10."
15:  # If there's no argument, use the current time.
16:  try:
17:    oclock = argv[1]
18:  except IndexError:
19:    oclock = strftime('%I:%M')
20:  
21:  hour, minute = [ int(x) for x in oclock.split(':') ]
22:  
23:  # Round to the nearest half-hour, because there are no
24:  # emoji clockfaces for other times.
25:  rminute = int(round(float(minute)/30)*30)
26:  if rminute == 60:
27:    rminute = 0
28:    hour = 1 if hour == 12 else hour + 1
29:  
30:  print clocks['%d:%02d' % (hour, rminute)]

The dictionary is defined in Lines 7–12. By including the encoding line at the top of the script, I was able to include the clock faces directly in the source code instead of messing around with hex character codes.

But the script itself isn’t the point of this post. What I really want to talk about is how I wrote it. I knew what Lines 7–12 were going to look like, and I really didn’t want to type it out. Not only would it be tedious, I’d almost certainly make an error somewhere. So I used a combination of IPython and BBEdit’s search-and-replace tools to build Lines 7–12 step by step.

Step 1 was generating a list of times. I used these commands in an IPython session to print a list of times, one per line:

python:
for h in range(1, 13):
  print "'%d:00'\n'%d:30'" % (h, h)

I copied this into a BBEdit window, moved the last two lines, 12:00 and 12:30, to the top, and did this find-and-replace:

BBEdit replacement dialog

This gave me a single line that looked like this,

12:00', '12:30', '1:00', '1:30', '2:00', '2:30', '3:00', '3:30',
'4:00', '4:30', '5:00', '5:30', '6:00', '6:30', '7:00', '7:30',
'8:00', '8:30', '9:00', '9:30', '10:00', '10:30', '11:00', '11:30', '

which I’ve broken up here for ease of reading. I cleaned up the front and rear to turn it into a Python list assignment,

python:
times = ['12:00', '12:30', '1:00', '1:30', '2:00', '2:30', '3:00',
'3:30', '4:00', '4:30', '5:00', '5:30', '6:00', '6:30', '7:00',
'7:30', '8:00', '8:30', '9:00', '9:30', '10:00', '10:30', '11:00', '11:30']

which I entered into my IPython session.

Step 2 was to do a similar thing for the clock faces. I “typed” them in, one per line, using the Character Viewer,

Character Viewer

and then converted them into a list assignment that I entered into IPython.

python:
faces = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓',
'🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤',
'🕙', '🕥', '🕚', '🕦']

Now comes the fun part. With times and faces defined, it was easy to generate most of the dictionary assignment statement with

python:
print ",  ".join("'%s': '%s'" % (t, f) for t, f in zip(times, faces) )

That gave me this string

'12:00': '🕛',  '12:30': '🕧',  '1:00': '🕐',  '1:30': '🕜',
'2:00': '🕑',  '2:30': '🕝',  '3:00': '🕒',  '3:30': '🕞',
'4:00': '🕓',  '4:30': '🕟',  '5:00': '🕔',  '5:30': '🕠',
'6:00': '🕕',  '6:30': '🕡',  '7:00': '🕖',  '7:30': '🕢',
'8:00': '🕗',  '8:30': '🕣',  '9:00': '🕘',  '9:30': '🕤',
'10:00': '🕙',  '10:30': '🕥',  '11:00': '🕚',  '11:30': '🕦'

where, again, I’ve added line breaks here to make it easier to read.

A few quick edits turned that string into

python:
clocks = {'12:00': '🕛', '12:30': '🕧',  '1:00': '🕐',  '1:30': '🕜',
           '2:00': '🕑',  '2:30': '🕝',  '3:00': '🕒',  '3:30': '🕞',
           '4:00': '🕓',  '4:30': '🕟',  '5:00': '🕔',  '5:30': '🕠',
           '6:00': '🕕',  '6:30': '🕡',  '7:00': '🕖',  '7:30': '🕢',
           '8:00': '🕗',  '8:30': '🕣',  '9:00': '🕘',  '9:30': '🕤',
          '10:00': '🕙', '10:30': '🕥', '11:00': '🕚', '11:30': '🕦'}

which is the nicely formatted dictionary assignment statement you see in Lines 7–12.

Does this seem like more work than just typing it out? It wasn’t. One of the great things about writing scripts frequently is that you become fluent enough to know instinctively how cobble together little bits of code like this. You also learn that some things, like the beginning and end of a list assignment statement, are more efficiently done by typing than by programming, so you don’t waste time trying to script those parts.

Sometimes your first instinct is a little off. I initially wrote the line that built the dictionary string this way:

python:
print ", ".join("'%s': '%s'" % (t, f) for t, f in zip(times, faces) )

Do you see the difference? There’s only one space after the comma in the joining string instead of two. When I saw the output of this statement, I realized that to get the times lined up nicely I’d need a mixture of one and two spaces, depending on whether the hour was a one- or two-digit number. Since there are nine one-digit hours and only three two-digit hours, it would be faster to put two spaces everywhere and delete one in front of the two-digit hours. I reran the line with two spaces after the comma and saved myself twelve edits.

I’m sure there are still better ways to do this, further tricks that would have saved me even more typing. But even though I didn’t save as much time as I might have, I still saved a lot, mainly because I automated all the quotes, colons, and commas that I tend to mistype.


  1. It is useful for Rob. He makes links in FoldingText to his timed Reminders and includes the appropriate clock face in the link text.