You really like me

If you’re like me, you tend to think of automation as a way to streamline the performance of complex, many-step actions. And while that’s certainly an important use of automation, sometimes its the simple things, things that barely seem worthy of automating, that are the most satisfying. Today I used Shortcuts to replace a bit of lost functionality in Tweetbot that’s been bugging me for quite a while.

Once upon a time in Tweetbot, you could see who liked one of your tweets by swiping to the left to see it by itself and then tapping on the number of likes.

Tweetbot view of a tweet

You can still do this with retweets, but not with likes. As a fundamentally insecure person, desperate for the approval of others, I miss being able to quickly see who liked my tweets. That’s what my “You Really Like Me” shortcut addresses.1

Here it is, a Share Sheet shortcut with just two steps:

You Really Like Me shortcut steps

It takes advantage of Twitter’s simple URL system. If you want to see the likes of the tweet with URL

you just stick /likes onto the end of it:

That’s what the first step of the shortcut does. The second step shows the web page for that URL.

List of likes after running shortcut

Boom. Lost functionality replaced. To be sure, what used to be a single tap in Tweetbot is now three (Share→Shortcuts→You Really Like Me), but at least I don’t have to mess around with launching another app and either pasting in a URL or navigating through my tweets.

Update Oct 6, 2018 10:13 AM  I didn’t include a download link for this shortcut, because I figured it was too short to bother. Then I heard from Sebastian Peitsch:

@drdrang Everything’s different in German but I managed to get this done via comparing the icons

Thanks Doc!

  — Sebastian Peitsch (@SPeitsch) Sat Oct 6 2018 2:57 PM

If you’re using Shortcuts in a different language, the names of the actions aren’t the same, which makes it harder to copy than it should be. But an imported shortcut will be localized to your language. So here’s a link to download: You Really Like Me.

Thanks to Sebastian for the help.

Update Oct 6, 2018 10:30 AM  If you find that the shortcut isn’t working for you, that it opens the tweet in a browser view but not the list of likes, it’s because you aren’t logged in to Twitter in that browser. I thought I had tested it under those conditions, but I hadn’t. Thanks to Grant Buell for finding the error and Sean for explaining it.

I should also point out that John Cutler has a two-tap system for getting to his likes:

@oscargong1995 @drdrang @OpenerApp Tapping the time/date stamp on a tweet in Tweetbot will also open the tweet in the mobile twitter site. As long as you’re logged in, you’ll be able to view likes there too.
  — John Cutler (@JohnCutler) Sat Oct 6 2018 1:46 PM

This takes one less tap, but I find it takes slightly more time than my shortcut, as it loads the entire mobile Twitter site after you tap on the time/date stamp. Still, you might prefer it.

  1. With apologies to Sister Bertrille. 

Shortcuts as subroutines

I was too busy at work last week to spend any time with Federico Viticci’s excellent screenshot framing shortcut for iOS, but I dug into it over the weekend and used it as the basis for a shortcut that does the things I usually need done after creating a screenshot:

I renamed Federico’s shortcut from “XS Frames” to “Frame Screenshot.” My extended shortcut is called “Frame and Upload” and I keep it next to “Frame Screenshot” in my list of shortcuts to make it easy to find.

It would typically be called from the Share Sheet within the Photos app after selecting one or more screenshot images to frame. After selecting “Frame and Upload” from the Shortcuts list, the screenshots are processed and then the user is asked to resize the resulting image.

Resize dialog

Because I wrote this to handle images posted here on the blog, and because width is the controlling dimension, the resizing is done by specifying a new width—the height is adjusted automatically to maintain the aspect ratio.

Note that the current width of the framed screenshot is given as the default. If the user just taps the OK button without editing the width field, no resizing is done.

Next, the user is asked for a file name.

File name dialog

Actually, what gets entered here is just a part of the file name. I have a particular naming scheme I use for images here:

yyyymmdd-Description of image.ext

The date the image is uploaded, like 20181002, is included as a prefix. Then comes a description of the image, which can, and usually does, include spaces—this is the part that the user is asked for. I have other scripts that extract this portion of the file name to use as the alt and title attributes in the <img> tag. The extension for these framed screenshots is png.

Assuming everything goes well—as Federico said in his post, the shortcut sometimes fails, probably for lack of memory; I’ve always found that I could run it successfully immediately after a failure—I have a framed screenshot in Photos and a new file in the blog’s images directory on the server.

First and second home screens

Here’s the complete shortcut, with the various sections marked:

Frame and Upload

Now you can see why I’m not interested in specifying the height of an image.

A few things worth expanding on:

Chances are you don’t want to enter all these steps by hand. You can download a template of it and just change the stuff in the SSH action.

I’m still not a big fan of the Workflow/Shortcuts programming environment—I prefer typing to dragging, and really prefer a text environment when I need to edit a program. But there’s no question of its power, especially when you can take entire shortcuts and use them as subroutines. Thanks to the Workflow boys for that. I have a distinct sense that if Shortcuts had been developed within Apple from the start, that feature, essential for building up complex programs, would not have been included.

At present, Federico’s framing shortcut can add only X🅂 and X🅂 Max frames around your screenshots. I assume he didn’t bother with iPad frames because he’s expecting new versions to arrive soon and didn’t want to waste his time on something that would have to be changed almost immediately. Presumably, when Steve Troughton-Smith and Guilherme Rambo uncover the hero images for the new iPads, Federico will be on them like a duck on a June bug. Do they have June bugs in Italy?

Redoing my diary

You’ve probably heard the computer science saying “You can write Fortran in any language.” It’s a recognition that when people move to a new system, they tend to hang on to ideas they learned under their old system. This sort of conservatism isn’t a sin—there’s also “if it ain’t broke, don’t fix it”—but it does prevent you from taking advantage of all the new system has to offer.

When I watched this June’s WWDC keynote, like many people, I was excited by Shortcuts, and that excitement continued through the summer as I heard more about it from the people who were using the iOS 12 beta. I had a particular set of automations that I thought would be improved by implementing it in Shortcuts, and I figured that would be the first thing I worked on after updating to iOS 12. And it was, but after a couple of days of playing with it, I realized that recasting the automations in Shortcuts was just writing Fortran in another language. What I ended up with is a big improvement on my old system, but it isn’t anything like what I was imagining all summer. And it doesn’t even use Shortcuts.

What I wanted to improve was my old system for creating entries in an elementary diary, meant mainly to keep track of my time on days I was traveling for work. The system was built around a Drafts action and a set of Launch Center Pro buttons. Whenever I left the office, got to a job site, arrived at a hotel, etc., I would tap the corresponding button in Launch Center Pro. The buttons were keyed to URL schemes that would create a new draft, add the appropriate text (“Leaving work,”, “At site,” “At hotel,” etc.), and then run a Drafts action. The Drafts action would append the draft and a time stamp to a Dropbox file named for the date (and would create that file if it didn’t already exist).

It was a nice system, and I’d been using it successfully for four years. It seemed natural to replace the Launch Center Pro buttons with Shortcuts so I could talk to Siri and just say “Leaving work,” “At site,” “At hotel,” etc. But I soon realized that the main difference between the me of today and the me of four years ago is not iOS 12 and the advent of Siri Shortcuts, it’s my wearing an Watch.

A Shortcut activated by “Hey Siri, leaving work” works if I’m talking to my iPhone or, less likely, my iPad, but it won’t work if I’m talking to my Watch, because the watch can’t run Drafts actions.

Shortcut attempt on watch

Now there’s no question but that saying “Hey Siri, leaving work” to my phone is an improvement over waking it up, starting Launch Center Pro, and tapping the “Leaving work” button, but it still forces me to have my phone out of my pocket, and I really want to be able to make diary entries through my watch. After some thinking and spending more time with the Drafts documentation (RTFM really is good advice), I think I have a solution that works the way I want.

First, I went to the settings in Drafts on my phone (the ⚙︎ icon in the lower right corner) and configured the Drafts watch app this way:

Drafts Apple Watch settings

Turing on the autocapture setting means that after I tap the Drafts complication (I use the Modular face), Drafts opens waiting for me to dictate. Most important is the “diary” tag attached to every draft made through the watch. With this setup, every time I dictate into my phone through the Drafts app, I’ll create a new draft with the “diary” tag in my Drafts Inbox. All these drafts can be accessed from either my iPhone or iPad. More important, all these drafts have metadata that can be processed from my iPhone or iPad.

In Drafts, I have an action group for work-related actions:

Work action group

When I’m done with a trip, I run the “Assemble diary” action, which collects all of the Inbox drafts tagged “diary” and lays them out in a new summary draft. Here’s an example from entries I made yesterday during a day in Chicago with my wife:

Diary as of 2018-09-23 07:32 -0500

2018-09-22 10:29 -0500
41.779527, -88.145043
At train station

2018-09-22 11:13 -0500
41.823875, -87.833883

2018-09-22 11:39 -0500
41.87075, -87.632507
Union station

2018-09-22 12:12 -0500
41.879616, -87.623718
Art institute

2018-09-22 14:06 -0500
41.88059, -87.624397
Launch at Pratt

2018-09-22 15:19 -0500
41.88875, -87.624678
Apple Store

2018-09-22 16:18 -0500
41.858962, -87.633807
Ping tom park

2018-09-22 19:51 -0500
41.879122, -87.638562
Union station again

Each entry is preceded by the date, time, and location at which its draft was created.1 The accuracy of the transcription is par for the course with Siri (we had lunch at Pret A Manger), but it’s usually close enough to remember what you meant. What makes this work is the metadata that Drafts saves with each draft.

The “Assemble diary” action has just one step, this JavaScript:

 1:  // Start a new draft
 4:  // Get all the "diary" drafts in the inbox
 5:  var diaryDrafts = Draft.query("", "inbox", ["diary"]);
 7:  // Assemble
 8:  var ds = draft.processTemplate("[[date|%Y-%m-%d %H:%M %z]]");
 9:  var diaryContent = "Diary as of " + ds + "\n\n";
10:  for (var i=0; i<diaryDrafts.length; i++) {
11:    var stamp = diaryDrafts[i].processTemplate("[[created|%Y-%m-%d %H:%M %z]]") + "\n" + diaryDrafts[i].processTemplate("[[created_latitude]], [[created_longitude]]") + "\n";
12:    diaryContent += stamp + diaryDrafts[i].content + "\n\n";
13:    diaryDrafts[i].isArchived = true;
14:    diaryDrafts[i].update();
15:  }
17:  // Make the diary
18:  editor.setText(diaryContent);

The comments explain most of what’s going on, but here’s a bit more detail:

  1. Line 5 uses a query to collect references to all the Inbox drafts tagged “diary.” The first parameter to the query function is a string to search for. Since we want all such drafts, regardless of content, this parameter is an empty string. The third parameter is supposed to be a list of all the tags to search on, which is why "diary" is in brackets.
  2. Lines 8 and 11 use the processTemplate function to access the metadata. Line 8 gets the current (when the script is run) date and time to put at the top of the new draft. Line 11, which is in the loop that runs for each diary entry draft, gets the date and time as well as the latitude and longitude that the diary draft was created.
  3. Lines 13 and 14 move the diary draft that’s the current subject of the loop into the Archive. The update is needed to ensure that the change sticks when the script is done. This cleans out the Inbox but leaves the diary drafts still accessible in case you need them again.

The keys to this script—the reasons it’s even possible—are the metadata that Drafts saves with each draft and the ways Drafts makes that metadata available to users. These features allow me to make diary entry drafts on the go from the watch and then include the times and locations later on when assembling the summary.

Eventually, I’ll have a bunch of old diary entry drafts in my Archive and it would be nice to have an efficient way to get rid of them. The “Trash archived diary entries” script does that. It’s also a single-step script action:

1:  // List of all archived diary drafts
2:  var diaryDrafts = Draft.query("", "archive", ["diary"]);
4:  // Trash them
5:  for (var i=0; i<diaryDrafts.length; i++) {
6:    diaryDrafts[i].isTrashed = true;
7:    diaryDrafts[i].update();
8:  }

This new system is distinctly better than the old one. It’s more flexible in that I can make entries with any content, and it uses the Watch the way it’s meant to be used, as a way to do small things without pulling your phone out of your pocket.

The annoying thing is that I could have done all this months ago, right after I got my watch. But I was stuck in a rut of thinking one way—using URL schemes or Workflow/Shortcuts to launch Drafts actions as each diary entry was made—that I didn’t see the better approach that was there all the time.

  1. The location is given as a latitude, longitude pair on a line under the date and time. I don’t expect to use this often (after all, I know where my office is), but I thought it might be helpful in some situations. The values have to be taken with a grain of salt, as the location accuracy can be poor when you’re inside. 

PCalc binomial functions—Part 2

Last time, we defined the probability mass function (PMF) for the binomial distribution in PCalc. This time we’ll do (sort of) the binomial’s cumulative distribution function (CDF).

As you recall, the PMF is defined this way:

[\binom{N}{n}\; p^n\; (1 - p)^{N-n}]

and is the probability that there will be [n] successes in [N] independent trials when the probability of success in any given trial is [p]. The CDF is the probability of no more than [n] successes in [N] independent trials when the probability of success in any given trial is [p]. In math, we can write it this way:

[\sum_{k=0}^n \: \binom{N}{k}\; p^k\; (1 - p)^{N-k}]

Basically, we do the PMF calculation for every number of successes from 0 through [n] and we add them together. The binomial CDF appears in lots of practical probability problems, so it would be useful to have it as a PCalc function.

Unfortunately, I don’t think a real binomial CDF function is possible in PCalc because PCalc can’t loop. It has a variety of Skip operations,

Skip operations in PCalc

which work like GOTO statements combined, in all but the first case, with a Boolean test. You might think these could be used to set up loops, but they allow forward skips only, not backward skips, and I can’t think of any way to get a loop going if the flow of operations can’t go backward.1

So we can’t write a function that will calculate the binomial CDF in one call, but we can write one that allows us to quickly run through the summation for each value of [k], accumulating the sum as we go. We’ll call the function “Binomial CDF Step.”

Here’s how it works. Start by putting a zero on the stack as our initial value of the sum. Then add [n], [N], and [p], so the stack looks like this:

Binomial Step 0

After the first call to Binomial CDF Step, the stack will look like this:

Binomial Step 1

What’s happened is we did the PMF calculation for [k=5], added its result to the cumulative sum (which was zero), and reset the stack for another round with [k=4]. Everything is set for a second call to Binomial CDF Step, after which the stack will look like this:

Binomial Step 2

Another call leads to this:

Binomial Step 3

Another gives us this:

Binomial Step 4

And another:

Binomial Step 5

We’re now ready to do the last PMF calculation, with [k=0].2 After this last step, we get this:

Binomial Step 6

Because there are no more calculations to do, the stack has been reduced to just the sum.

While this is a less than ideal way to run through a loop, it goes surprisingly quickly. PCalc helps out by always putting the last function run at the top of the list you choose from after tapping the ƒ(x) button. Would I ever use this to calculate a CDF for [n=50]? No, but it’s quite handy for small values of [n].

The Binomial CDF Step function is defined this way:

Binomial CDF Step

It’s basically the Binomial PMF function from the last post with a bit of business at the end that resets the stack for the next trip through the loop. You can enter it by hand for the full PCalc programming experience, or just download it.

  1. I think James Thomson has written that PCalc’s functions were implemented initially to support unit conversions, where loops aren’t necessary. 

  2. The way the summation sign is written, it’s natural to think of the loop as incrementing k from 0 to n, but addition is commutative, so it’s perfectly correct to decrement k from n down to 0.