Ringing “Half-sheets” with Perl 6
The problem statement
I have data in a source in the cloud. I want to create a PDF using that data.
The background
One of my hobbies is English Change Ringing.
Change ringing is a particular type of ringing, which arose in England, which instead of ringing melodies, rings permutations.
It’s difficult to explain, but I’ll try to give an overview in the next few slides.
Make the sound travel
When a bell is struck by either a clapper or a hammer, the noise travels primarily in the direction the mouth of the bell is facing. For bells hung “dead” (such as carillon bells), this means that most of their volume goes down, into the ground. If, instead, you put a bell on a wheel, you can get it to strike when pointing outwards, thus make it audible much further away.
However, it takes a lot of energy to keep a bell on a regular wheel going. In England, from the 1200s to the 1600s, they started adding extra pieces of wood (called the “stay” and the “slider”) that let the bells be stored mouth up, and developed a way of ringing the bells that used potential energy to save a lot of effort.
There are some nice animations here showing bells moving.
There is one disadvantage though; when the bell is rung this way, it takes around 2 seconds from when it is struck to when it can be struck again. This makes ringing melodic tunes difficult (if not impossible).
Something to do with all these bells…
Instead of attempting to ring melodies, English ringers developed a system of ringing which permutes the order of the bells.
You start ringing a descending scale. You then start permuting the order of the bells, using a memorized “method”, so that each time all of the ringing bells have stuck, it is in a different order. The method eventually will return you to the descending scale, without repeating any permutation.
(Technically, there are two different styles of change ringing: “method” ringing, and “called changes”. I am only describing “method” ringing here.)
Methods
A method is a way of changing the order of the bells. Some of them are simple, and some of them are complex. Every methods starts from “rounds” (the descending scale) and proceeds through a set of changes until it arrives back at rounds. At various points in the change, a call can be made. A call (usually “bob” or “single”) changes the pattern for one change, after which the ringers resume the pattern (although in a new position). Calls allow methods to be extended to make “touches”, which are longer (or shorter) than the “plain course” of a method.
For example, here is a very simple method, called “Plain Bob Doubles” (“doubles” means that it is rung on five bells).
Or, a more complex one, called “Cambridge Surprise Major” (“major” is rung on eight bells; “surprise” is a category of methods “that have internal places made when the treble changes dodging pairs”).
So what does it sound like
Here are a bunch of YouTube videos of ringing. We can watch a few of them, if you want…
Performances
In ringing, if you ring a “touch” that comprises all of the possible permutations for the number of bells you are ringing on, it is called ringing an “extent”.
From math, we know that the number of permutations of a given number of items is n!
. So, 3 bells = 6 changes, 4 bells = 24 changes, etc.
Ringing an extent of 7 bells is 5040 changes. This takes about 3 hours (give or take). Three hours of ringing, with no breaks or substitutions, is a reasonable “hard” thing to do (for many people, it is not really hard). So this became known as a “peal”. On 8 or more bells, anything over 5000 changes is considered a peal.
(The extent on 8 (40,320 changes) has been rung in tower (as opposed to handbells) in 1963; it took 18 hours, on very light bells.)
Three hours, though, can be hard to fit into a day, and if the ringing is spotty, very annoying for the neighbors. Much more common is a “quarter-peal”, which is anything from 1260 changes (1250 on 8 or more bells) to 5040 changes (5000 on 8 or more bells). This usually takes around 45 minutes to ring.
BellBoard
Having spent three hours (or forty-five minutes) concentrating on counting to the exclusion of everything else, you want to make sure people know you did this. Towers have local documentation (in the form of peal books, etc.), but it is nice to get recognition from others. (Historically, that should read “be able to brag to others, and force them to buy you beer, or get into fights with them”; ringing has calmed down a lot in the last several centuries.)
As a result, we share our accomplishments. Since 1911, one way to do this has been The Ringing World, a weekly publication that has columns, letters, and is about half composed of peal and quarter-peal notices.
In addition, they now run a site called “BellBoard”, which is a web-based system that lets you submit your records, and have them included in the print version of The Ringing World. (Presumably, you can also send them in by mail.)
Dedications
Often, quarter-peals or peals are dedicated to someone or something. These are included as footnotes in the records, and are for things like deaths, birthdays, weddings, etc.
When my grandfather died in 2009, my sister and I put together a quarter-peal in his honor. After we rang the quarter-peal, I made an overly formal announcement of it, in the style of something from the erzatz 18th century, printed it on nice paper, put in in a presentation folder, and gave it to my grandmother.
I did this on several other occasions as well (e.g.). But it was a pain to type everything out each time, and I never really got into the habit of doing it on a regular basis.
Bragging to the tourists
We have two towers in Boston: the Church of the Advent, and Christ Church in the City of Boston (which most people know as “Old North”).
Old North has tours that come through the ringing room, where they learn a little bit about ringing.
(Old North has the oldest set of change ringing bells in North America, installed in 1745. Paul Revere was one of the first ringers!)
We want to show the tourists that we are still ringing, and give them a sense that this is a living tradition. We have put up pictures, and try to change them from time to time. It was suggested that we could put up copies of interesting performances we do.
This gave me the impetus to try and create a program to automate creating ringing announcements. Which is what I'm going to talk about now.
One last thing…
Well, almost. Here are some links to things about ringing that might be of interest, if you want to digress:
- North American Guild of Change Ringers (their intro pamphlet)
- Central Council of Church Bell Ringers
- Ringing.info (all kinds of links)
- A presentation on some of the mathematics in change ringing
- I don't understand this at all
- more pretty pictures, which mean nothing to me
- ringing shapes
- a video by a quandam (and hopefully future) Boston ringer (Bill may enjoy this)
- Knitting changes
The data
So, let’s start with the data.
The data is kept on BellBoard, https://bb.ringingworld.co.uk. For example, this quarter-peal in honor of a Boston ringer who died in World War I, or this Easter peal with a number of firsts.
Although I have written programs to scrape web pages for data, it kind of sucks. However, this page documents their “API”. From this, we can see that we can get the data in XML fairly easily.
XML examples
Here is some example XML.
From the quarter-peal:
<?xml version="1.0"?> <performance xmlns="http://bb.ringingworld.co.uk/NS/performances#" id="P1244942"> <association></association> <place towerbase-id="5852"> <place-name type="place">Boston</place-name> <place-name type="dedication">Christ Church, Old North</place-name> <place-name type="county">Massachusetts</place-name> <ring type="tower" tenor="13-3-5 in F" /> </place> <date>2018-09-02</date> <duration>45m</duration> <title><changes>1260</changes> <method>Stedman Doubles</method></title> <ringers> <ringer bell="1">Laura Dickerson</ringer> <ringer bell="2" conductor="true">Elaine M Hansen</ringer> <ringer bell="3">Phoebe House</ringer> <ringer bell="4">Joshua R Burson</ringer> <ringer bell="5">Michael Tartell</ringer> <ringer bell="6">Bryn Marie Reinstadler</ringer> <ringer bell="7">Austin J Paul</ringer> <ringer bell="8">Katarina Whimsy!</ringer> </ringers> <footnote>First of Stedman: 4, 5</footnote> <footnote>First in tower: 6, 8</footnote> <footnote>Rung in memory of Boston Guild ringer Private Murray Edward Gordon Mackman (26 July, 1890 - 1 September, 1918)</footnote> <donation>£3.50</donation> <timestamp>2018-09-03T19:02:49</timestamp> </performance>
And from the peal:
<?xml version="1.0"?> <performance xmlns="http://bb.ringingworld.co.uk/NS/performances#" id="P1225038"> <association>North American Guild</association> <place towerbase-id="5852"> <place-name type="place">Boston</place-name> <place-name type="dedication">Christ Church, Old North</place-name> <place-name type="county">Massachusetts</place-name> <ring type="tower" tenor="13-3-5 in F" /> </place> <date>2018-04-01</date> <title><changes>5088</changes> <method>Yorkshire Surprise Major</method></title> <composition ref="2266172" /> <composer role="composed">Donald F Morrison</composer> <ringers> <ringer bell="1">Cally D Perry</ringer> <ringer bell="2">T David Westmoreland</ringer> <ringer bell="3">Alison Stevens</ringer> <ringer bell="4">Leland Paul Kusmer</ringer> <ringer bell="5" conductor="true">Edward J Futcher</ringer> <ringer bell="6">John Bihn</ringer> <ringer bell="7">Elaine M Hansen</ringer> <ringer bell="8">Austin J Paul</ringer> </ringers> <footnote>Rung for Easter.</footnote> <footnote>First peal in the method 3,4,6</footnote> <timestamp>2018-04-02T22:33:37</timestamp> <rwref>5586.476</rwref> </performance>
The PDFs
From the XML data, I want to generate PDFs. However, sometimes I want to modify the data before creating the PDF; for instance, to change the date to read “Easter, 2018”, or to fix the extra spacing in the quarter-peal footnote.
This means that I need some intermediate format, ideally text-based for easy editing, that can be turned into a PDF easily.
In particular, these were the requirements that I came up with:
- text-based
- can be turned into a PDF with not too much difficulty
- lets me layout the page, not just flow text
- not too difficult to learn
Text based intermediate format options
Here are the various things that I considered:
HTML + css
- Although there is support for using HTML + css to create printed material (see this article, for example), there isn’t a lot of support for this yet.
SVG + css
- Although would be cool, it suffers from the same problem as HTML, which is that it is difficult to turn into a PDF easily.
(La)TeX
- Probably a good choice, although my impression is that getting the kind of page control I want is more complicated. However, it’s a huge download (1G).
DocBook
- Even less clear how to do proper layout than HTML, and have you ever tried to get it installed?
roff
- Already installed on my computer; probably not too hard to learn; has good page control…
I could probably have made TeX work, but I decided to look at roff
, because it was already on my computer.
(There are probably a bunch of other solutions, but they didn’t occur to me...)
roff
roff
is a text-based format, currently mostly used for generating man pages on Unix, but that has a history of page design. It’s fairly flexible, and the GNU version supports output directly to PDF.
There are some annoyances that I ran into: in particular, it is difficult to get it to work with system fonts (that is, it is impossible to work with system fonts, you need to translate them into some special format and put them in a particular place, which I don’t have a license to do with the font I wanted to use). Also, support for Unicode is complicated (although this is partially because PDF support is complicated as well...).
roff template
The first step I took was to develop a roff file to generate the ringing half-sheet.
Here is a commented version of the template.
As you can see in the initial comment, you turn this into a PDF using the command:
groff -Tpdf -Kutf8 demotroff.groff > demotroff.pdf
Guild logos
You’ll notice that there is a logo in the upper-right hand of the generated file. This is a precreated PDF image that is embedded in the output PDF.
To create these PDFs, I took existing logos, and manually recreated them in SVG. For instance, here is the Boston Change Ringers logo as an SVG (and the SVG source).
To convert them into PDFs, I wound up using a web-based conversion service (as I mentioned, going from SVG to PDF is not easy). In particular, I used FileFormat.Info, which works ok. (Its support for non-PS-standard fonts is a little lacking; the Boston Change Ringers font was converted incorrectly. In basically everything fonts (or, more accurately, typefaces) are complicated.)
Perl 6; the glue beneath my wings
So, now I have a roff template file, all I need to do is fill it in. I usually reach for Perl 5 at this point, but I thought that this would be a nice project to use Perl 6 for.
(And, thus, this is why this talk is happening.)
The prologue
#!/usr/bin/env perl6 use v6; sub croak { note $^msg; exit(1); } # because Perl 6 doesn't have the Perl 5 "\n" magic for die use HTTP::UserAgent; use XML::XPath; use Template::Mustache;
There isn’t much here to talk about. croak()
fixes what I see as a problem in the Perl 6 version of die()
.
HTTP::UserAgent
will be used to retrieve data from BellBoardXML::XPath
will allow me to extract data from XMLTemplate::Mustache
is a templating engine, not too different from Uri’s Perl 5Template::Simple
, which I have become somewhat used to
MAIN()
The MAIN()
subroutine is a formal entry point to the program, if you want one (you don’t need to provide it). The advantage of using it is that you can specify command line parameters.
sub MAIN ( Str :p(:$performance)!, Bool :g(:$groff) = False, Bool :f(:$force) = False, Str :i(:$image)? where ( !$image.defined or ($image eq 'none') or "{$image}.pdf".IO.f or croak("{$image}.pdf does not exist for inclusion as image") ) ) { my $file = "groff/$performance.groff"; if $file.IO.e and !$force { croak "File $file already exists. Will not overwrite."; } my $xml = get-performance-xml($performance); my %parsed = parse-performance-xml($xml); my $output = create-groff(%parsed, $image); if %parsed<pid> ne $performance { croak "Retrieved performance has different id: requested $performance and received %parsed<pid>. Will not continue."; } # save the data $file.IO.spurt: $output; say "$file written."; if $groff { shell "/usr/local/bin/groff -Tpdf -Kutf8 $file > pdf/$performance.pdf"; say "groff command run."; } }
- the aliases in the function signature allow you to have long and short names for variables (and you can have multiple aliases, if you want) [main] [aliases]
- you can have required parameters (
!
), default values (= False
), and optional values (?
) [args] - you can use the
where
clause to ensure that the values are valid [where] - note that if you want to validate optional variables, one of the tests must be to let it not be there, and you have to manually
die
if it isn’t valid [constr opt] - any string can be coerced into an
IO
object [io] - note that we are calling
spurt
using the indirect invocant syntax [spurt] [:] - instead of using backticks, use the
shell
routine [shell]
get-performance-xml()
This retrieves the data from BellBoard.
sub get-performance-xml ($p) { my $data = HTTP::UserAgent.new.get("https://bb.ringingworld.co.uk/view.php?id={$p}", Accept => 'application/xml'); $data.is-success or croak("HTTP error retrieving post: {$data.status-line}."); return $data.content; }
- nothing to say here, except to note the use of the
Accept
header to retrieve the XML as per the API - One caveat is that you cannot quote the keys of named parameters when passing them as a pair; this really annoys me [traps]
parse-performance-xml()
This takes the XML from BellBoard, and pulls out the various variables that we will need.
sub parse-performance-xml ($xml) { my $xpath = XML::XPath.new(xml => $xml); my %data; # gather the straightforward items my %spec = ( 'pid' => 'substring(/performance/@id,2)', # the performance ID sadly starts with a 'P' in the XML 'guild' => '/performance/association/text()', 'date' => '/performance/date/text()', 'tower' => '/performance/place/@towerbase-id', 'towernamepl' => '/performance/place/place-name[@type="place"]/text()', 'towernamede' => '/performance/place/place-name[@type="dedication"]/text()', 'towernameco' => '/performance/place/place-name[@type="county"]/text()', 'nchanges' => '/performance/title/changes/text()', 'method' => '/performance/title/method/text()', 'composer' => '/performance/composer/text()', 'details' => '/performance/details/text()', 'notes' => '/performance/footnote/text()', ); for %spec.kv -> $k, $v { my $r = $xpath.find($v); given $r.WHAT { when XML::Text { %data{$k} = $r.text().trim(); } when Str { %data{$k} = $r; } when Array { %data{$k} = $r.map({ .text().trim(); }).list; } default { if defined($r) { croak("Unknown type {$_.perl} for key $k"); } } } } # gather the ringers for | $xpath.find('/performance/ringers/ringer') -> $r { my $ringer = $r.contents().map( {.text().trim();}).join(' '); if $r.attribs<conductor> { $ringer ~= ' \*[conductor]'; } %data<ringers>{$r.attribs<bell>} = $ringer; } return %data; }
- this is using XPath to pull the data I want out of the XML data
- note that I can get both the key and value out of the hash at the same time in the
for
loop [for] [.kv] - I use the return type of the XPath search to determine what I need to do to store it [WHAT]
.list
makes sure that the call tomap()
doesn’t return a lazy sequence [map] [seq] [list]- with
.list
::notes($("Rung for Easter.", "First peal in the method 3,4,6"))
- without
.list
::notes($(("Rung for Easter.", "First peal in the method 3,4,6").Seq))
- with
- the
|
is theslip
operator, which unwraps the array object, so that you can iterate over it [|]
create-groff()
This takes the data extracted from the XML, uses it to fill out the template, and generates the output roff.
sub create-groff (%perf, $image) { my %rdata; %rdata<pid> = %perf<pid>; %rdata<urpic><img> = do given %perf<guild> { when defined($image) { $image }; when 'North American Guild' { 'nagcr' }; when 'MIT Guild of Bellringers' { 'bcr' }; when 'Boston Change Ringers' { 'bcr' }; default { 'none' }; }; if (%rdata<urpic><img> eq 'none') { %rdata<urpic> = Nil; }
- you can use
do given
to assign data to a variable [do] [given] - but your tests don’t have to be related to the topicalized value [when] [given/when]
- you should use
Nil
(case matters) instead ofundef
if you want to assign an undefined value [undef] [Nil]
if %perf<guild> { %rdata<guild><guild> = %perf<guild>; } %rdata<date> = Date.new(%perf<date>, formatter => &date-formatter);
- the
Date
object accepts a formatter parameter [Date] [formatter]
my $towername = "%perf<towernamede>, %perf<towernamepl>, %perf<towernameco>"; if %perf<tower> ~~ (5851|5852) { %rdata<tower>{'t' ~ %perf<tower>}<towername> = $towername; } else { %rdata<tower><tdef><towername> = $towername; }
- I use a junction to test if the tower is one of the two Boston towers, because in that case I will do something special later on in the template [(|)]
%rdata<performance_type> = do given %perf<nchanges> { when $_ < 1250 { 'performance' }; when 1250 <= $_ < 5000 { 'quarter-peal' }; when 5000 <= $_ { 'peal' }; default { 'weird non-number of changes' }; }; %rdata<method><method> = "%perf<nchanges> %perf<method>"; if %perf<composer> { %rdata<method><composed><composer> = %perf<composer>; } if %perf<details> { %rdata<method><details><details> = %perf<details>; } for %perf<ringers>.keys.sort(&infix:«<=>») -> $n { %rdata<ringers>.push: { num => $n, ringer => %perf<ringers>{$n} }; } %rdata<ringers>[0]<num> = '\*[treble]'; my $num = numbells(%perf<method>); if ($num % 2 == 0) || (%rdata<ringers>.elems >= $num) { %rdata<ringers>[* - 1]<num> = '\*[tenor]'; }
sort
defaults to sorting as a string, and the keys of the ringers array are strings (because they came from XML), so I need to force it to use the<=>
comparison [sort]- this section needs to be reworked a bit; there must be a better way to generate the
ringers
list (I originally was doing something more complex here, but it was determined to be wrong) - accessing the last element of an array is now done via
[*-1]
, instead of a negative index [*-1]
if %perf<notes>.elems { %rdata<notes><footnotes> = [ %perf<notes>.map({ %( 'note' => $_ ); }) ]; }
- this line is annoying; you can’t use
{ }
to create a hash if you are in amap
block; you have to use%( )
. However, you aren’t supposed to use that to create hashes in certain other contexts, and it all gets too complicated [%()]
my $out = Template::Mustache.render($=finish, %rdata, :literal); $out ~~ s:g/ \n ** 2..* /\n/; # clean up blank lines, which are anathema to troff $out .= trans([ '<', '>', '&', '"' ] => [ '<', '>', '&', '"' ]); # fix XML entities return $out; }
date-formatter()
This lets me format dates the way I want, with affixes. I found the algorithm on StackOverflow.
sub date-formatter ($self) { my $year = $self.year; my $month = qw<nul January February March April May June July August September October November December>[$self.month]; my $day = $self.day; # see https://stackoverflow.com/a/13627586/1030573 my $day_m10 = $day mod 10; my $day_m100 = $day mod 100; my $affix = '\*[th]'; # default affix if ($day_m10 == 1) && ($day_m100 != 11) { $affix = '\*[st]'; } elsif ($day_m10 == 2) && ($day_m100 != 12) { $affix = '\*[nd]'; } elsif ($day_m10 == 3) && ($day_m100 != 13) { $affix = '\*[rd]'; } return "$month $day$affix, $year"; }
numbells()
Earlier, I wanted to only make the last bell marked as the “tenor” if in an “even” method, or if there were more bells ringing than the number of bells “inside” the method. To know how many bells are inside bells, you need to transform the last word of the method name back into a number.
Note that this will not work for “called changes”, I haven’t yet added support for that. In order to properly indicate the tenor, I may need to manually fix the .groff
file.
sub numbells ($method) { my $stage = ($method ~~ m/(\w+)$/).Str; my @counts = qw<nul impossible impossible Singles Minimus Doubles Minor Triples Major Caters Royal Cinques Maximus>.map: &fc; return @counts.first(fc($stage), :k) // 16; }
- to get back the stage of the method, I use the
:k
option for thefirst()
method to return the index of the matching element [first]
The template
This is just the roff template we went through earlier, but removing all of the comments, and putting in the various template data.
Because of how Template::Mustache
works [mustache], I can create certain “custom” towers (see the area around the {{# towers}}
block), which lets me do something different for e.g. Old North.
=finish \# in 'root' dir: groff -Tpdf -Kutf8 groff/{{& pid}}.groff > pdf/{{& pid }}.pdf \X'papersize=5.5in,8.5in' .pl 8.5i .po 0.5i .ll 4.5i .sp |0.5i .nr def_ps 10 .nr def_vs 15 .nr sml_ps 9 .nr sml_vs 13 .nr flr_ps 20 .nr flr_vs 20 .fam N .ft NR .ps \n[def_ps]pt .vs \n[def_vs]pt .lg .kern .nh .de GUILD .ps \\n[def_ps]pt .vs \\n[def_vs]pt .nop \f[I]for the\f[]\h[|0.5i]\f[B]\\$1\f[] .. .de STANZA .ps \\n[def_ps]pt .vs \\n[def_vs]pt .sp .in 0 .ft NI .nop \\$1 .br .in 0.5i .ft NR .. .de ftsmall .ps \\n[sml_ps]pt .vs \\n[sml_vs]pt .. .de finalflourish .ps \\n[flr_ps]pt .vs \\n[flr_vs]pt .sp .in 0 .ce .nop \f[ZD]\m[red3]\N[167]\m[]\f[] .br .. .ds st \f[B]\v[-.25v]\s[-4]st\s[+4]\v[.25v]\f[] .ds nd \f[B]\v[-.25v]\s[-4]nd\s[+4]\v[.25v]\f[] .ds rd \f[B]\v[-.25v]\s[-4]rd\s[+4]\v[.25v]\f[] .ds th \f[B]\v[-.25v]\s[-4]th\s[+4]\v[.25v]\f[] .ds treble \s[-3]TREBLE\s[+3] .ds tenor \s[-3]TENOR\s[+3] .ds conductor \f[I]\s[-1](conductor)\s[+1]\f[] .nf {{# urpic}} \h[|3.0i]\X'pdf: pdfpic {{& img }}.pdf -L 1.5i 1.5i' .sp 0.5v {{/ urpic}} {{# guild}} .GUILD "{{& guild }}" {{/ guild}} .STANZA "on" {{& date }} .STANZA "at" {{# tower }} {{# t5851 }} The Church of the Advent {{/ t5851 }} {{# t5852 }} Christ Church in the City of Boston .ftsmall \f[I](called \[lq]Old North\[rq])\f[] {{/ t5852 }} {{# tdef }} {{& towername }} {{/ tdef }} {{/ tower }} .STANZA "was rung a {{& performance_type }} of" {{# method }} {{& method }} {{# composed }} .ftsmall composed by {{& composer }} {{/ composed }} {{# details }} .fi .ftsmall {{& details }} .nf {{/ details }} {{/ method }} .STANZA "by the ringers" .in 0 .ta 1iR 1.2i {{# ringers }} {{& num }} {{& ringer }} {{/ ringers }} .br {{# notes}} .STANZA "with notes" .fi {{# footnotes }} {{& note }} .sp 0.25 {{/ footnotes }} .nf {{/ notes}} .finalflourish
All at once
Output
Earlier, I mentioned two performances, a quarter-peal and a peal.
Here are the various steps for each:
September quarter-peal
- on BellBoard
- as XML
- as roff (with edits)
- as PDF
Easter peal
Next
There are some things that I still need and want to do with this...
- If possible, I’d like to support called changes notes.
- I need to fix the BCR logo PDF so that it has the correct font.
- Once Perl 6 fixes the issue with POD variables [= twigil] not being implemented, I’d like to support both a simple and a fancy output style (harkening back to the original PDFs I made).
- I want to figure out how to get a better font available (with more interesting ligatures, etc.). This may involve purchasing a nicer font.
- I’m not entirely happy with the fleuron I’m using; Hœfler text has a better one, but licensing issues…