Calendar layouts

As I read Lukas Mathis’s critique of Google’s new calendar app for Android, I came across a passage that triggered one of my pet peeves: my firm belief that calendar apps are designed by people who don’t use calendars.

Lukas is complaining that the new Android calendar shows only 5 days at a time in its “week” view and scrolls forward and back by that same amount:

It’s now a «5 Day» view, because it only shows five days. This is confusing, because it means that the starting day in this view changes. Instead of always being Monday (or Sunday in the US), it’s now a random day.

Lukas’s criticism is spot-on, but his parenthetical remark is dead wrong. Weekly calendar/planners in the US start on Mondays. You can look at Day Runner, Day-Timer, At-A-Glance;1 They all start on Monday and have for the 30+ years I’ve been paying attention. Monthly calendars in the US do start on Sundays, but never weekly calendars. (At this point, I’d normally make a cutting remark about how Europeans think they understand American culture from watching movies and TV shows, but I’m taking the high road today.)

So where did Lukas get the idea that Americans like to start their weekly calendars on Sunday? Probably from the poorly designed calendar software we’re forced to use. Here are the settings for BusyCal and Apple’s Calendar:

BusyCal and Calendar week start settings

They let you start the week on any day—accommodating the untold millions of people who start their work week on a Thursday—but the setting applies to both monthly and weekly views. And they do this despite a longstanding history of paper calendars in which monthly and weekly views start on different days.

Google’s web calendar is more restrictive in what it allows you to choose as the beginning of the week, which makes you think some actual thought was put into it, but it, too, forces you to use the same start day for monthly and weekly views.

Google week start setting

I have no experience with Microsoft Outlook. Maybe it allows you to set different start days for different views, but from what I can see on tutorial pages like this one, I doubt it.

The great thing about software is its fluidity. With paper calendars, you pretty much have to choose whether you’re a daily, a weekly, or a monthly person—switching between different views on paper requires too much copying and recopying. Switching views in a software calendar is just a click away.

But for some reason, designers of software calendars have not seen fit to give us that one extra option that would make our calendar layouts match the way most of us in the US prefer to work. I can only assume this is because they don’t use the weekly view themselves, and their ideas of how it should be laid out come from looking at other software, not direct experience. They should spend a few bucks on paper calendars and learn what decades of experience has taught those designers.

Update 11/24/14 9:49 AM
Turns out™ you can get Apple’s Calendar to start its week on a Monday in week view and on a Sunday in month view, but it’s still not the solution I’d like to see. As David Rosenblum explained in a tweet this morning:

@drdrang open week view, then scroll horizontally. You can position the week view to start on any day. Works on OS X and iOS.
David Rosenblum (@TweetByDavid) Nov 24 2014 9:39 AM

What’s nice about this solution is that once you have week view aligned to Monday, subsequent presses of ⌘→ and ⌘← will scroll forward and backward seven days, maintaining the Monday alignment. What’s less nice is that if you shift to month view and then back to week view again (or if you quit and restart Calendar) the alignment goes back to Sunday, and you have to realign it again. Because I think being able to shift smoothly between the two views is important, I don’t like having to do this extra fiddling.

Still, it’s better than what BusyCal does. With BusyCal’s starting day set to Sunday, you can get its week view to align on a Monday by either ⌥-scrolling or pressing ⌥⌘→. Unfortunately, this doesn’t “stick.” Subsequent presses of ⌘→ and ⌘← realign the weekly layout to Sunday. And, as you might expect, the week view realigns to Sunday when you shift to month view and then back.

My solution to this is to just stay in month view. When I’m on the phone with a client trying to schedule a meeting, it’s usually “when are you free in late January, early February?” not “when are you free later this week?” so month view makes the most sense most of the time. A good part of the reason I use BusyCal is that I prefer the way its month view looks.

I’d like to be able to flip between month and week views regularly, but I just can’t get used to seeing weekly calendars that start on Sunday or monthly calendars that start on Monday. And I shouldn’t have to. The computer is supposed to adapt to my way of working; it doesn’t have to break 30 years of habit.


  1. Am I the only one who didn’t know that all of these once-proud, independent brands are now owned by Mead? An insidious calendrical monopoly is creeping over the country, preparing to force us all into using Trapper Keepers. 


Aligning text tables on the decimal point

I’ve recently written several reports with tables of numbers, and as I made more and more tables of this type, it became clear that my Normalize Table filter for BBEdit needed some updating. Now it can align numbers on the decimal point.

Here’s an example. Suppose I start with a table in MultMarkdown format

| Very long description | Short | Intermediate |
|.--.|.--.|.--.|
| 17,750 | 9,240.152 | 7,253.0 |
| 3,517 | 13,600.0 | 6,675 |
| 18,580.586 | 8,353.3 | 13,107.3 |
| 7,725 | 355.3 | 14,823.2 |
| None | 10,721.6 | 7,713 |
| 12,779.1592 | 14,583.867 | 3,153.2 |
| 18,850.03 | -4,756.6686 | 13,081.74 |
| -2,264.80 | 13,772.729 | 12,557 |
| -17,141.001 | 26,227.27 | — |
| 2,772.35 | 14,772.1509 | -3,814.0 |
| 934 | 20,853 | -10,272.2 |
| 8,082.139 | 7,864.0048 | 1,583.010 |

This is legal, but it’s impossible to read as plain text. Since readability is the chief virtue of Markdown, I want to adjust the spacing so all the columns line up. The older version of my filter worked well for left-aligned, right-aligned, and centered columns, but it made no attempt to align numeric columns on the decimal point. Now it does. After running the above through the Normalize Table filter, it comes out looking like this:

| Very long description |    Short    | Intermediate |
|.---------------------.|.-----------.|.------------.|
|       17,750          |  9,240.152  |   7,253.0    |
|        3,517          | 13,600.0    |   6,675      |
|       18,580.586      |  8,353.3    |  13,107.3    |
|        7,725          |    355.3    |  14,823.2    |
|          None         | 10,721.6    |   7,713      |
|       12,779.1592     | 14,583.867  |   3,153.2    |
|       18,850.03       | -4,756.6686 |  13,081.74   |
|       -2,264.80       | 13,772.729  |  12,557      |
|      -17,141.001      | 26,227.27   |      —       |
|        2,772.35       | 14,772.1509 |  -3,814.0    |
|          934          | 20,853      | -10,272.2    |
|        8,082.139      |  7,864.0048 |   1,583.010  |

The key features of the new filter are:

  1. Decimal points are aligned (duh).
  2. Numbers without decimal points are right-aligned just before the decimal point.
  3. Decimal-aligned lists of figures are centered in their columns.
  4. Entries that aren’t numbers are also centered in their columns.

The decimal points at either end of the format line (.----.) tell the filter to use decimal alignment on that column. I should point out that the extra spaces the filter adds have no effect on the processed output.

HTML doesn’t have a good and widely supported way to align table columns on decimal points, but that’s OK, because my reports get transformed into LaTeX, not HTML, and LaTeX has a nice dcolumn package for decimal alignment.

Here’s the new version of the filter:

python:
  1:  #!/usr/bin/python
  2:  
  3:  import sys
  4:  import re
  5:  
  6:  decimal = re.compile(r'^( -?[0-9,]*)\.(\d* )$')
  7:  integer = re.compile(r'^( -?[0-9,]+) $')
  8:  
  9:  def just(string, type, n, b, d):
 10:    "Justify a string to length n according to type."
 11:    
 12:    if type == '::':
 13:      return string.center(n)
 14:    elif type == '-:':
 15:      return string.rjust(n)
 16:    elif type == ':-':
 17:      return string.ljust(n)
 18:    elif type == '..':
 19:      isdec = decimal.search(string)
 20:      isint = integer.search(string)
 21:      if isdec:
 22:        before = len(isdec.group(1))
 23:        after = len(isdec.group(2))
 24:        string = ' ' * (b - before) + string + ' ' * (d - after)
 25:      elif isint:
 26:        before = len(isint.group(1))
 27:        string = ' ' * (b - before) + string + ' ' * d
 28:      return string.center(n)
 29:    else:
 30:      return string
 31:  
 32:  
 33:  def normtable(text):
 34:    "Aligns the vertical bars in a text table."
 35:    
 36:    # Start by turning the text into a list of lines.
 37:    lines = text.splitlines()
 38:    rows = len(lines)
 39:    
 40:    # Figure out the cell formatting.
 41:    # First, find the separator line.
 42:    for i in range(rows):
 43:      if set(lines[i]).issubset('|:.- '):
 44:        formatline = lines[i]
 45:        formatrow = i
 46:        break
 47:    
 48:    # Delete the separator line from the content.
 49:    del lines[formatrow]
 50:    
 51:    # Determine how each column is to be justified.
 52:    formatline = formatline.strip(' ')
 53:    if formatline[0] == '|': formatline = formatline[1:]
 54:    if formatline[-1] == '|': formatline = formatline[:-1]
 55:    fstrings = formatline.split('|')
 56:    justify = []
 57:    for cell in fstrings:
 58:      ends = cell[0] + cell[-1]
 59:      if ends in ['::', ':-', '-:', '..']:
 60:        justify.append(ends)
 61:      else:
 62:        justify.append(':-')
 63:    
 64:    # Assume the number of columns in the separator line is the number
 65:    # for the entire table.
 66:    columns = len(justify)
 67:    
 68:    # Extract the content into a matrix.
 69:    content = []
 70:    for line in lines:
 71:      line = line.strip(' ')
 72:      if line[0] == '|': line = line[1:]
 73:      if line[-1] == '|': line = line[:-1]
 74:      cells = line.split('|')
 75:      # Put exactly one space at each end as "bumpers."
 76:      linecontent = [ ' ' + x.strip() + ' ' for x in cells ]
 77:      content.append(linecontent)
 78:    
 79:    # Append cells to rows that don't have enough.
 80:    rows = len(content)
 81:    for i in range(rows):
 82:      while len(content[i]) < columns:
 83:        content[i].append('')
 84:    
 85:    # Get the width of the content in each column. The minimum width will
 86:    # be 2, because that's the shortest length of a formatting string and
 87:    # because that matches an empty column with "bumper" spaces.
 88:    widths = [2] * columns
 89:    beforedots = [0] * columns
 90:    afterdots = [0] * columns
 91:    for row in content:
 92:      for i in range(columns):
 93:        isdec = decimal.search(row[i])
 94:        isint = integer.search(row[i])
 95:        if isdec:
 96:          beforedots[i] = max(len(isdec.group(1)), beforedots[i])
 97:          afterdots[i] = max(len(isdec.group(2)), afterdots[i])
 98:        elif isint:
 99:          beforedots[i] = max(len(isint.group(1)), beforedots[i])
100:        widths[i] = max(len(row[i]), beforedots[i] + afterdots[i] + 1, widths[i])
101:        
102:    # Add whitespace to make all the columns the same width. 
103:    formatted = []
104:    for row in content:
105:      formatted.append('|' + '|'.join([ just(s, t, n, b, d) for (s, t, n, b, d) in zip(row, justify, widths, beforedots, afterdots) ]) + '|')
106:    
107:    # Recreate the format line with the appropriate column widths.
108:    formatline = '|' + '|'.join([ s[0] + '-'*(n-2) + s[-1] for (s, n) in zip(justify, widths) ]) + '|'
109:    
110:    # Insert the formatline back into the table.
111:    formatted.insert(formatrow, formatline)
112:    
113:    # Return the formatted table.
114:    return '\n'.join(formatted)
115:  
116:          
117:  # Read the input, process, and print.
118:  unformatted = unicode(sys.stdin.read(), "utf-8")
119:  print normtable(unformatted)

The main additions come in Lines 88–100 and Lines 18–28. Instead of just tracking the length of each cell, the filter now also tracks the number of characters before and after the decimal point.1. The overall width of the column is then either the longest text entry (including the header entry) or the sum of the longest lengths before and after the decimal point. If necessary, extra spaces are added before and after the number to create a new string that’s then centered in column.

Although I use this in BBEdit, there’s no reason it couldn’t be adapted to other intelligent text editors. It uses standard input and standard output, so it ought to be easy to incorporate into a Sublime Text, TextMate, Vim, or Emacs workflow.


  1. If you use commas as your decimal indicator, you’ll have to edit the filter a bit to get it to work for you. Be aware, though, that you’ll still need to use periods in the formatting line—MultiMarkdown doesn’t allow commas there. 


Sitemap evolution

I don’t know how valuable a sitemap really is, but I decided I might as well have one. When I was running ANIAT on WordPress, the WP system (or maybe it was a plugin) made one for me, but now I need to build one on my own and keep it updated with each new post. I put together a simple system today and figured it was worth sharing how I did it.

A sitemap is an XML file that is, at its heart, just a list of URLs to all the pages on your site. ANIAT is build from a directory structure that looks like this,

Local copy of ANIAT

where source is the directory of Markdown source files, site is the directory of HTML and support files that’s mirrored to the server, and bin is the directory of scripts that build the site. The Makefile runs the scripts that build and upload the site.

Starting in the bin directory, I got a list of all the posts with a simple ls command:

ls ../site/*/*/*/index.html

This returned a long list of file paths that looked like this:

../site/2014/11/more-rss-mess/index.html
../site/2014/11/post-post-post/index.html
../site/2014/11/snell-scripting/index.html
../site/2014/11/the-rss-mess/index.html
../site/2014/11/three-prices/index.html

The simplest way to turn these into URLs was by piping them through awk,

ls ../site/*/*/*/index.html | awk -F/ '{printf "http://www.leancrew.com/all-this/%s/%s/%s/\n", $3, $4, $5}'

which returned lines like this:

http://www.leancrew.com/all-this/2014/11/more-rss-mess/
http://www.leancrew.com/all-this/2014/11/post-post-post/
http://www.leancrew.com/all-this/2014/11/snell-scripting/
http://www.leancrew.com/all-this/2014/11/the-rss-mess/
http://www.leancrew.com/all-this/2014/11/three-prices/

The -F/ option told awk to split each line on the slashes, and the $3, $4, and $5 are the third, fourth, and fifth fields of the split line.

With this working, it was easy to beef up the awk command to include the surrounding XML tags:

ls ../site/*/*/*/index.html | awk -F/ '{printf "<url>\n  <loc>http://www.leancrew.com/all-this/%s/%s/%s/</loc>\n</url>\n", $3, $4, $5}'

This gave me lines like this:

<url>
  <loc>http://www.leancrew.com/all-this/2014/11/more-rss-mess/</loc>
</url>
<url>
    <loc>http://www.leancrew.com/all-this/2014/11/post-post-post/</loc>
</url>
<url>
  <loc>http://www.leancrew.com/all-this/2014/11/snell-scripting/</loc>
</url>
<url>
  <loc>http://www.leancrew.com/all-this/2014/11/the-rss-mess/</loc>
</url>
<url>
  <loc>http://www.leancrew.com/all-this/2014/11/three-prices/</loc>
</url>

Now all I needed were a few opening and closing lines, and I’d have a sitemap file with all the required elements.

The awk commands were growing too unwieldy to maintain as a one-liner, so I put them in a script file and added the necessary parts to the beginning and the end.

 1:  #!/usr/bin/awk -F/ -f
 2:  
 3:  BEGIN {
 4:    print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n<url>\n  <loc>http://www.leancrew.com/all-this/</loc>\n</url>"
 5:  }
 6:  {
 7:    printf "<url>\n  <loc>http://www.leancrew.com/all-this/%s/%s/%s/</loc>\n</url>\n", $3, $4, $5
 8:    }
 9:  END {
10:    print "</urlset>"
11:  }

With this script, called buildSitemap, I could run

ls ../site/*/*/*/index.html | ./buildSitemap > ../site/sitemap.xml

and generate a sitemap.xml file in the site directory, ready to be uploaded to the server. The file looked like this:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
  <loc>http://www.leancrew.com/all-this/</loc>
</url>
.
.
.
<url>
  <loc>http://www.leancrew.com/all-this/2014/11/the-rss-mess/</loc>
</url>
<url>
  <loc>http://www.leancrew.com/all-this/2014/11/three-prices/</loc>
</url>
</urlset>

This was the output I wanted, but the awk script looked ridiculous, and if I wanted to update script to add <lastmod> elements to each URL, it was just going to get worse. I’m not good enough at awk to make a clean script that’s easy to maintain and improve. So I rewrote buildSitemap in Python:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import sys
 4:  
 5:  print '''<?xml version="1.0" encoding="UTF-8"?>
 6:  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
 7:  <url>
 8:    <loc>http://www.leancrew.com/all-this/</loc>
 9:  </url>'''
10:  
11:  for f in sys.stdin:
12:    parts = f.split('/')
13:    print '''<url>
14:    <loc>http://www.leancrew.com/all-this/{}/{}/{}/</loc>
15:  </url>'''.format(*parts[2:5])
16:    
17:  print '</urlset>'

It’s a little longer, but for me it’s much easier to understand at a glance. In fact, it was fairly easy to figure out how to add <lastmod> elements:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import sys
 4:  import os.path
 5:  import time
 6:  
 7:  lastmod = time.strftime('%Y-%m-%d', time.localtime())
 8:  print '''<?xml version="1.0" encoding="UTF-8"?>
 9:  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
10:  <url>
11:    <loc>http://www.leancrew.com/all-this/</loc>
12:    <lastmod>{}</lastmod>
13:  </url>'''.format(lastmod)
14:  
15:  for f in sys.stdin:
16:    parts = f.split('/')
17:    mtime = time.localtime(os.path.getmtime(f.rstrip()))
18:    lastmod = time.strftime('%Y-%m-%d', mtime)
19:    print '''<url>
20:    <loc>http://www.leancrew.com/all-this/{1}/{2}/{3}/</loc>
21:    <lastmod>{0}</lastmod>
22:  </url>'''.format(lastmod, *parts[2:5])
23:    
24:  print '</urlset>'

The rstrip() in Line 17 gets rid of the trailing newline of f, and os.path.getmtime returns the modification time of the file in seconds since the Unix epoch. This is converted to a struct_time by time.localtime() and to a formatted date string in Line 18 by time.strftime(). The same idea is used in Line 7 to get the datestamp for the home page, but I cheat a bit by just using the current time, which is what time.localtime() returns when it’s given no argument.

Now the same pipeline,

ls ../site/*/*/*/index.html | ./buildSitemap > ../site/sitemap.xml

gives me a sitemap with more information, built from a script that’s easier to read. I’ve added this line to one of the Makefile recipes in bin. When I add or update a post, the sitemap gets built and is then mirrored to the server.

You could argue that the time I spent using awk was wasted, but I don’t see it that way. It was a quick way to get preliminary results directly from the command line, and in doing so I learned what I needed in the final script. If I were to do it over again, I’d still use awk in the early stages, but I’d shift to Python one step earlier. The awk script was a mistake, and I should have recognized that as soon as the BEGIN clause got unwieldy.

Update 11/21/14 10:29 PM
As Conrad O’Connell pointed out on Twitter,

@drdrang the sitemap file doesn’t do you much good unless you add it to your robots.txt file

Re: leancrew.com/all-this/2014/…

Conrad O’Connell (@conradoconnell) Nov 21 2014 5:13 PM

you do need to let the search engines know that you have a sitemap and where it is. The various ways you can do that are given on the sitemaps.org protocol page. I used this site to ping the search engines after validating my sitemap, but I agree with Conrad that adding a

Sitemap: http://path/to/sitemap.xml

line to the robots.txt file is probably a better idea because it doesn’t require you to know which search tools use sitemaps.


Three prices

When I saw (on Twitter, of course) that Mike Nichols had died, I thought of this sketch. Which isn’t surprising—I think of it often.

Jack Paar introduces it as an indictment of the funeral business—his mention of Jessica Mitford was a reference to the first edition of her The American Way of Death—and it certainly is that, but it’s also a sly poke at the three-tier pricing model lots of retailers use to get you “buy up.”

The low end of Apple’s current 16/64/128 GB storage option for the iPhone and iPad is the equivalent of “two men who… do God knows what with him.” Like Mike Nichols, we go for the one in the middle.