Cutting the cord

You may have seen Steve Mould’s recent video, called “The Spring Paradox.” If not, take a few minutes, and we’ll reconvene when you’re done.

As you might expect, I’m not interested in the traffic problem that takes up the second half of the video. But the spring problem that gives the video its name is cute, and Mould does a good job explaining how the behavior is related to the difference between springs in series and springs in parallel. But…

Surely the weight won’t rise under all conditions. If the two longer strings, the red and green ones, were much longer—or if the springs were much stiffer—the weight would drop. So what restrictions did Mould have to adhere to when he set up his demonstration?

Let’s start by drawing the two states next to one another. Before cutting is on the left and after cutting is on the right.

Springs and strings from Mould video

(The strings should be straight on the right, but I’ve drawn them curved so they don’t overlap.)

Our starting assumptions are:

  1. All the strings are inextensible.
  2. The long red and green strings are the same length, \(L_{\ell}\).
  3. The short blue string is of length \(L_s\).
  4. The springs have the same unstretched length, \(L_0\).
  5. The springs follow Hooke’s Law and have the same stiffness, \(k\).

Recall that Hooke’s Law says that the stretch in a spring, \(x\) is related to the force acting on it, \(F\), through the simple linear relationship

\[F = k x\]

Strictly speaking, we could relax the last condition and say only that the two springs must have the same force-deflection relationship. It doesn’t have to be a linear relationship, but that will make the algebra easier.1

In the “before” state, each spring is carrying a the full weight, \(W\), and is stretched a distance \(\frac{W}{k}\). Therefore, the distance between the ceiling and the weight is

\[2 \left( L_0 + \frac{W}{k} \right) + L_s\]

where \(L_s\) is the length of the short blue string.

In the “after” state, each spring is carrying half the weight, and is stretched a distance \(\frac{W}{2k}\). Therefore, the distance between the ceiling and the weight is

\[L_{\ell} + \left( L_0 + \frac{W}{2k} \right)\]

For the weight to go up when the blue string is cut, we must have

\[L_{\ell} + \left( L_0 + \frac{W}{2k} \right) < 2 \left( L_0 + \frac{W}{k} \right) + L_s\]

or, after some algebra,

\[L_{\ell} < L_0 + L_s + \frac{3}{2}\frac{W}{k}\]

This sets the upper bound on the lengths of the red and green strings if we want the weight to behave as it does in the video. If the strings are longer than this, the weight will go down when the blue string is cut.

There’s also a lower bound on \(L_{\ell}\). The problem starts with slack in the red and green strings, so

\[L_{\ell} > \left( L_0 + \frac{W}{k} \right) + L_s\]

Putting the two restrictions together, we have

\[L_0 + L_s + \frac{W}{k} < L_{\ell} < L_0 + L_s + \frac{3}{2}\frac{W}{k}\]

So to get the behavior we see in the video, the lengths of the red and green strings have to be within a range of

\[\frac{1}{2}\frac{W}{k}\]

In the video, this is a relatively long range, because the springs are pretty stretchy. But if the strings were stiff, you’d have to be quite precise in setting their lengths. In the limit as \(k \rightarrow \infty\), which is what we’d see if we replaced the springs with inextensible strings, upward movement would be impossible—we can’t achieve both inequalities.

This, I suspect, is part of the reason the behavior seems weird to us. When we see the initial setup, our brains tend to think of the springs as being of a particular length that won’t change. We don’t expect them to shorten by more than the amount of slack in the red and green strings. Maybe if we had seen the springs at their unstretched length before the video started, we wouldn’t be as surprised to see them contract and the weight move up.

One last calculation: how much does the weight move up? Subtracting the “after” length from the “before” length gives us

\[y = \left[ 2 \left( L_0 + \frac{W}{k} \right) + L_s \right] - \left[ L_{\ell} + \left( L_0 + \frac{W}{2k} \right) \right]\]

which is

\[y = L_0 + L_s - L_{\ell} + \frac{3}{2}\frac{W}{k}\]

Solving this equation for \(L_{\ell}\) and substituting into the inequality above gives us

\[L_0 + L_s + \frac{W}{k} < L_0 + L_s + \frac{3}{2}\frac{W}{k} - y < L_0 + L_s + \frac{3}{2}\frac{W}{k}\]

After some cancellation, we get

\[-\frac{1}{2}\frac{W}{k} < -y < 0\]

which, after multiplying through by -1 and flipping the inequality signs, gives us

\[0 < y < \frac{1}{2}\frac{W}{k}\]

Is it a coincidence that the upper bound here is the same as the range of string length we calculated earlier? What do you think?

To get the most upward movement, we’d like the red and green strings to be at the short end of their range. But without much slack in the strings, the trick wouldn’t look as cool. The way to get a noticeable upward movement and significant slack in the strings is to use a stretchy spring, which is exactly what Mould does. Clever guy.


  1. In fact, as Mould points out, his springs are being stretched enough to act outside their linear range. The trick still works, but not exactly according to the calculations we do below. 


Trains and tables

For as long as I’ve had an iPhone, I’ve had a stripped-down version of the commuter train schedule between my town and Chicago. It started as a small set of web pages, then rotated through whatever text editing app was my current favorite, and finally went into Notes. Overall, the schedule has been pretty stable; I think I’ve had to change the actual data only once. But there were some pretty big changes made earlier this month, so I decided it was time to redo my copy.

Metra publishes the schedule of my line as a PDF with a set of tables: weekday, weekend, eastbound, and westbound. Here’s an image of one of the pages:

BNSF schedule PDF

To extract the data in the tables, I turned to a tool that I use fairly often in my job: Tabula. It’s built specifically to turn grids of information in a PDF into a nice CSV that you can import into all kinds of data analysis software. I didn’t need to do any analysis for the train schedules, but the conversion to CSV would make the table easy to edit into the form I wanted.

Tabula is an unusual app. It’s basically a web server that runs locally, but instead of forcing you to deal with the mess of server configuration, Tabula packages all that into a standard-looking Mac app. You double-click it, the server starts, and Safari (or whatever your default browser is) launches with a home page that asks you for a PDF to import. Very neat.

After Tabula has read the PDF, it presents it to you so you can select the table to extract.

Tabula table selection

After processing the stuff in the pink rectangle, it shows you what it found:

Tabula extracted data

If the form of the data isn’t what you expected, you can change some of the options in the left sidebar and try again. With the train schedule, which has a grid of lines between the entries, the Lattice option worked better than the default Stream method.

With the table saved as a CSV file, the editing was easy. I opened it in Numbers, deleted all the rows except those for Union Station and Naperville, deleted the trains that don’t stop in Naperville, and transposed the rows and columns to give me a table that was vertically oriented instead of horizontally.

This left me with all the parts of the schedule I wanted and none that I didn’t, but I still had a couple of tweaks to go before pasting the table into Notes. First, I wanted suffixes of a and p added to the times to distinguish between AM and PM. That would be time-consuming in Numbers but was a snap in BBEdit. I copied the table out of Numbers, pasted it into BBEdit, and used a combination of column selection and the Text‣Prefix/Suffix Lines… command to put the as and ps where they belong.

After pasting the edited data back into Numbers, I selected the rows for express trains and used ⌘B and ⌘I to make them bold and italic. That put everything in the format I wanted for pasting the table into Notes. Here’s what one of the schedules looks like on my iPhone:

Schedule in Notes

This is an example of what I call the “render unto Caesar” strategy. Although we all have a tendency to want to do every part of a task with just one tool, it’s often best to break the task up and use different tools for each part. In this case, it was Tabula for data extraction, Numbers for the big deletions, and BBEdit for the fine tuning. The keys to success in this strategy are being able to recognize which tool is best for each subtask and being able to move the results from one tool smoothly into the next.


From TextExpander to Keyboard Maestro… again

After a good bit of thinking, I canceled my TextExpander subscription today. This is not the first time I’ve left TextExpander—I dropped it when Smile first adopted a subscription payment model about five years ago and stayed away even when Smile listened to the complaints and lowered the subscription price.

Eventually, though, I returned. TextExpander was the only realistic snippet solution for iOS and iPadOS, and as I found myself writing more and more on my iPad, I couldn’t live without it. Also, I like making temporary snippets to handle common phrases—like the name of a product or a company—that appear often in my writing as I work on a particular project but will never be used after the project is finished. TextExpander has a very efficient way of adding new snippets.

Things have changed over the past few months. My M1 MacBook Air has brought me back to the Mac in a big way. I no longer write anything longer than a text or an email on my iPad, and I don’t expect that to change. So cross-platform expansion isn’t as important as it once was.1 And although Smile seems to have fixed the crashing problem I was having a month or two ago, I’m still leery of TextExpander’s reliance on a bespoke syncing service.

So I’m back to using Keyboard Maestro as my snippet expansion tool. It works well, and I didn’t have to do too much work to switch over. In a rare display of forethought, I didn’t delete my snippet macros. I had merely disabled them when I started using TextExpander again—now I just had to re-enable them.

Keyboard Maestro snippet groups

Yes, there were some snippets from TextExpander that I’d made in the past few years that needed to be moved over to Keyboard Maestro, but that didn’t take much time. Some were even improved in the translation.

And I decided to tackle the one big advantage TextExpander had over Keyboard Maestro: the ability to make a new snippet quickly. By combining AppleScript with Keyboard Maestro itself, I now have a way to make a KM snippet out of what’s on the clipboard.

For example, let’s say I’m writing a report about products made by Mxyzptlk Industries. To make a snippet for that name, I copy it to the clipboard and invoke my new Make Temporary Snippet from Clipboard macro. That brings up this window,

Make Snippet input window

where I can define the trigger (I chose “;mi”) and adjust the expansion if necessary. After clicking OK, I have a new snippet in my Snippet - Temporary group.

Mxyzptlk snippet

Here’s the macro that does it:

Make Snippet macro

The first step asks the user for the snippet information, prepopulating the expansion field with the contents of the clipboard. The second step does all the real work, running this AppleScript:

applescript:
 1:  tell application "Keyboard Maestro Engine"
 2:    set trigger to getvariable "snippetTrigger"
 3:    set expansion to getvariable "snippetExpansion"
 4:  end tell
 5:  
 6:  set triggerXML to "<dict>
 7:  <key>MacroTriggerType</key>
 8:  <string>TypedString</string>
 9:  <key>SimulateDeletes</key>
10:  <true/>
11:  <key>TypedString</key>
12:  <string>" & trigger & "</string>
13:  </dict>"
14:  
15:  set expansionXML to "<dict>
16:  <key>Action</key>
17:  <string>ByTyping</string>
18:  <key>MacroActionType</key>
19:  <string>InsertText</string>
20:  <key>TargetApplication</key>
21:  <dict/>
22:  <key>TargetingType</key>
23:  <string>Front</string>
24:  <key>Text</key>
25:  <string>" & expansion & "</string>
26:  </dict>"
27:  
28:  tell application "Keyboard Maestro"
29:    tell macro group "Snippet - Temporary"
30:      set m to make new macro with properties {name:expansion}
31:      tell m
32:        make new trigger with properties {xml:triggerXML}
33:        make new action with properties {xml:expansionXML}
34:      end tell
35:    end tell
36:  end tell

Lines 1–4 pull in the values of the variables set during the previous macro step. Lines 6–26 define the XML text that defines what will become the new macro’s trigger and action. Finally, Lines 28–36 create a new macro in the Snippet - Temporary group and define it according to the XML. I took the overall structure of this section of the script from the Keyboard Maestro Wiki.

How did I know the XML format? I created a test macro by hand, exported it, and opened the Test.kmmacros file in BBEdit. From there, it was easy to see the parts that defined the trigger and the action. I did a little editing to accommodate the inclusion of the trigger and expansion variables and pasted the result into the script.

Making a Keyboard Maestro macro that runs an AppleScript that creates a new Keyboard Maestro macro is a lot of fun. More important, though, is that it brings KM up to TE’s level when it comes to making new snippets. Now I’m not making any compromises in using Keyboard Maestro for text expansion.

Update 07/11/2021 8:47 AM
Unsurprisingly, there is prior art in the “quick creation of a Keyboard Maestro snippet” field. This tweet from @DasPretzels led me to two macros that do basically what mine does: this one from Tom and this one from Peter Lewis himself. They both have a significant improvement on mine, in that they start with a Copy step, which eliminates one keystroke. So I added that to the top of my macro and also added a Delete Current System Keyboard step to the end. This leaves the clipboard in the same state it was before the macro was invoked.


  1. Even when I was still writing on my iPad, TextExpander had become less useful. At some point, fill-ins just stopped working, and I had to weaken several snippets to accommodate that loss of functionality. 


A stolen word count Quick Action

I mentioned yesterday that installing NetNewsWire brought back some old RSS feeds that I had mistakenly dropped somewhere along the road from the old NetNewsWire to Google Reader to my homemade RSS reader. One of them is Erica Sadun’s blog. A lot of what she writes is too deep into Xcode and real programming for me to understand, but her more elementary stuff is at my level.1

A few months ago, she wrote about a simple, two-step word-counting Quick Action she built in Automator. It takes whatever text you have selected—in any app—and pops up a window with the word and character counts.2

Sadun Word count

The first step is this shell script:

echo `echo $1 | wc -w` words. `echo $1 | wc -c` characters.

And the second step is this AppleScript:

applescript:
on run {input, parameters}
  display dialog input as string buttons {"OK"}
end run

After copying these steps outright, I decided to make a few changes. First, I noticed that the results window didn’t have a title, and the OK button wasn’t set to be the default—tapping the Return key wouldn’t dismiss it. So I made a couple of additions to the AppleScript:

applescript:
on run {input, parameters}
  display dialog input as text buttons {"OK"} default button 1 with title "Word Count"
end run

As for the shell script, I felt a little nervous about passing a long stretch of text in as an argument, so I decided to change the script to this:

wc -wc | awk '{printf "%d words and %d characters", $1, $2}'

and have the selected text come in as standard input instead of as an argument. The output of wc -wc is a string with a pair of numbers separated by whitespace. Awk is perfect for handling text like this because it reads stdin automatically, splits it on whitespace, and assigns the resulting substrings to the variables $1, $2, $3, etc.

This worked well, and if I were smart I would’ve stopped there. But I thought about using this Quick Action on longer stretches of text and how it wouldn’t format the numbers with commas at the hundreds/thousands boundary. Large counts would be easier to read with commas.

As it happens, awk’s printf command inherits a formatting code from the system printf that puts commas at the appropriate places. The code, unfortunately, is %'d, and the single straight quotation mark is a pain in the ass when constructing a shell command. I’d like to be able to use this in the pipeline:

awk '{printf "%'d words and %'d characters", $1, $2}'

but that won’t work because the shell interprets all the single quotation marks as string delimiters—the ones I want to use as formatting codes never get to awk.

As is often the case in shell scripting, there’s a way around this, but it’s incredibly confusing and ugly:

awk '{printf "%'"'"'d words and %'"'"'d characters", $1, $2}'

I found this solution here, and it took me a while to figure out how it works. Basically, it’s concatenating five separate strings, which you can probably see better if I color-code them:

'{printf "%'"'"'d words and %'"'"'d characters", $1, $2}'

Two of the strings (the blue ones) are delimited by single quotes and contain a double quote. Two others (the yellow ones) are delimited by double quotes and consist entirely of a single quote. Also, it’s important that the variables $1 and $2 are in single quotes to keep the shell from interpreting them before awk gets a chance to. When all of these are put together, this is the command string that awk sees:

{printf "%'d words and %'d characters", $1, $2}

Whew!

And that wasn’t the end of it. Even though this awk command worked at the command line, it didn’t work in Automator because Quick Actions don’t run in my normal command line environment.

The problem was with the locale, which isn’t set in the environment under which Quick Actions run.3 Luckily, the same web page that showed me the multiple quoting trick also showed me how to set the environment variable. Ultimately, the shell script step in my Quick Action was4

wc -wc | LC_ALL="en_US" awk '{printf "%'"'"'d words and %'"'"'d characters", $1, $2}'

Did I say “ultimately”? That was premature. The Quick Action worked fine with this shell script, but those five consecutive quotation marks bothered me. I knew I’d have trouble understanding them later (even if I had this blog post to explain them). I also knew that Python has a straightforward way to format numbers with commas. So I threw away the awk command and substituted in a longer, but easier to read, chunk of Python:

wc -wc | python3 -c 'import sys
w, c = map(int, sys.stdin.read().split())
print(f"{w:,d} words and {c:,d} characters")'

Python is a modular language and doesn’t automatically parse its input, so I needed to do some extra work on the front end. The second line reads in the standard input, splits it on whitespace, and converts the resulting strings into integers. That set up the variables w and c to be interpreted by the f-string in the print command. This is distinctly longer than the awk solution, but it’s also distinctly clearer.5

Here’s a screenshot of the Quick Action in Automator:

Word Count Quick Action

And here’s an example of its output:

Word count window

I hope Ms. Sadun forgives me for what I did to her simple automation.


  1. In this way, she’s a lot like Michael Tsai. I have to skip past many of his posts because they’re way over my head, but I stay subscribed for the ones I can follow. 

  2. For me, the value of this Quick Action isn’t for counting words I’m writing; BBEdit will tell me that in the status bar at the bottom of the window. This is for counting words in other people’s writing on (mainly) web pages. 

  3. You may have run into similar problems in which the Quick Action environment doesn’t set the PATH to what you expect. 

  4. Strictly speaking, setting LC_ALL is overkill. Just setting LC_NUMERIC to “en_US” would be sufficient to get the comma separators working. 

  5. Note that this script works only in Python 3. Apple has recently (starting with Catalina?) supplied Python 3 in addition to Python 2, but you have to install the Command Line Developer Tools to get it.