Category Archives: geek

2015-12-21 Emacs News

Links from reddit.com/r/emacs, /r/orgmode, Hacker News, planet.emacsen.org, Youtube, the Emacs commit log, the changes to the Emacs NEWS file, and emacs-devel.

Past Emacs News round-ups

Scripting and the grocery store flyer

We plan the week’s meals around the grocery store flyers, taking advantage of what’s on sale (chicken, ground beef, etc.) and stocking up when the opportunity presents itself (for example, diced tomatoes or cream of mushroom soup).

The flyers are usually delivered on Thursdays. We do most of our grocery shopping at No Frills because it’s convenient and almost always a good price, but sometimes we’ll go to Freshco or Metro if there’s a particularly good sale. I might flip through the other flyers if we’re looking for something in particular, but most of the time, we just toss them out. I’d love to opt out of paper flyers, but that doesn’t seem to be an option in our neighbourhood.

It doesn’t take a lot of time to review the flyers, but I figured it would be fun to write a script that highlights specific items for us. Now I have a script that parses the output of the No Frills accessible flyer to create a table like this:

Y Clementines 2.47 2 lb bag product of Spain $2.47
Y Smithfield Bacon 3.97 500 g selected varieties $3.97
Y Thomas’ Cinnamon Raisin Bread 2.5 675 g or Weston Kaisers 12’s selected varieties $5.00 or $2.50 ea.
Y Unico Tomatoes 0.97 796 mL or Beans 540 mL selected varieties $0.97
Fresh Boneless Skinless Chicken Breast 3.33 2.78 BIG Pack!™ DECEMBER 18TH – 24TH ONLY! $3.33 lb/$7.34/kg save $2.78/lb
Purex 3.97 2.02 2.03 L $3.97 save $2.02
Frozen Steelhead Trout Fillets 5.97 2.0 filets de truite $5.97 lb/$13.16/kg save $2.00/lb
Heinz Tomato Juice 0.97 1.52 1.36 L selected varieties $0.97 save $1.52
Nestlé Multi-Pack Chocolate or Bagged Chocolate 2.88 0.61 45-246 g selected varieties $2.88 save 61¢
Source 4.97 0.5 16 x 100 g selected varieties $4.97 save 50 ¢
Franco Gravy 0.67 0.32 284 mL selected varieties $0.67 save 32¢
Ocean Spray Cranberry Sauce 1.67 0.32 348 mL whole or jellied $1.67 save 32¢
10 lb Bag Yellow Potatoes, 5 lb Bag Carrots or 10 lb Bag Yellow Cooking Onions 1.87 product of Ontario, Canada no. 1 grade or 5 lb Bag Rutabaga product of Canada, no. 1 grade $1.87
Betty Crocker Hamburger Helper 1.5 158-255 g or Mashed or Scallop Potatoes 141-215 g selected varieties $3.00 or $1.50 ea.
Blackberries 1.25 6 oz product of U.S.A. or Mexico DECEMBER 18TH – 24TH ONLY! 4/$5.00 or $1.25 ea.

(more lines omitted)

The table is sorted by whether the item name matches one of the things we usually buy (first column: Y), then how much the sale is for, and then the name of the item. Over time, I’ll add more things to the priority list, and the script will get smarter and smarter.

I can use Org commands to move the rows up or down or remove the rows I’m not interested in. Then I can take the second column of the script’s output with Emacs’ copy-rectangle-as-kill command (C-x r M-w), and paste it into OurGroceries‘ import dialog. That builds a shopping list that’s sorted by the aisles I’ve previously set up, and this list is synchronized with our phones.

I’ve added the script to https://github.com/sachac/scripts/blob/master/check-grocery-flyer.js, so you can check there for updates. The flyer URL and the list of staples are defined in a separate configuration file that I haven’t included in the repository, but you can probably come up with your own if you want to adapt the idea. =)

Here’s the source, just in case:

#!/usr/bin/env node

/*
  Creates a prioritized list based on the flyers, like this:

Y Clementines 2.47    2 lb bag product of Spain $2.47
Y Smithfield Bacon  3.97    500 g selected varieties $3.97
Y Thomas' Cinnamon Raisin Bread 2.50    675 g or Weston Kaisers 12's selected varieties $5.00 or $2.50 ea.
Y Unico Tomatoes  0.97    796 mL or Beans 540 mL selected varieties $0.97
  Fresh Boneless Skinless Chicken Breast  3.33  2.78  BIG Pack!™ DECEMBER 18TH - 24TH ONLY! $3.33 lb/$7.34/kg save $2.78/lb
  Purex 3.97  2.02  2.03 L $3.97 save $2.02
  Frozen Steelhead Trout Fillets  5.97  2.00  filets de truite $5.97 lb/$13.16/kg save $2.00/lb
  Heinz Tomato Juice  0.97  1.52  1.36 L selected varieties $0.97 save $1.52
  Nestlé Multi-Pack Chocolate or Bagged Chocolate 2.88  0.61  45-246 g selected varieties $2.88 save 61¢
  ...
  */

var rp = require('request-promise');
var cheerio = require('cheerio');
var homeDir = require('home-dir');
var config = require(homeDir() + '/.secret');
var staples = config.grocery.staples; // array of lower-case text to match against flyer items
var flyerURL = config.grocery.flyerURL; // accessible URL

function parseValue(details) {
  var matches;
  var price;
  if ((matches = details.match(/\$([\.0-9]+)( | )+(ea|lb|\/kg)/i))) {
    price = matches[1];
  }
  else if ((matches = details.match(/\$([\.0-9]+)/i))) {
    price = matches[1];
  }
  else if ((matches = details.match(/([0-9]+) *¢/))) {
    price = parseInt(matches[1]) / 100.0;
  }
  return price;
}

function getFlyer(url) {
  return rp.get(url).then(function(response) {
    var $ = cheerio.load(response);
    var results = [];
    $('table[colspan="2"]').each(function() {
      var cells = $(this).find('td');
      // $0.67  or  2/$3.00 or $1.25ea
      var item = $(cells[0]).text().replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
      var details = $(cells[1]).text().replace(/([ \t\r\n\u00a0\u0000]| )+/g, ' ').replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
      var matches;
      var save = '';
      var price = parseValue(details);
      details = details.replace(/ \/ [^A-Z$]+/, ' ');
      if (details.match(/To Our Valued Customers/)) {
        details = details.replace(/To Our Valued Customers.*/, 'DELAYED');
      }
      if ((matches = details.match(/save .*/))) {
        save = parseValue(matches[0]);
      }
      results.push({item: item,
                    details: details,
                    price: price,
                    save: save});
    });
    return results;
  });
}

function prioritizeFlyer(data) {
  for (var i = 0; i < data.length; i++) {
    var name = data[i].item.toLowerCase();
    for (var j = 0; j < staples.length; j++) {
      if (name.match(staples[j])) {
        data[i].priority = true;
      }
    }
  }
  return data.sort(function(a, b) {
    if (a.priority && !b.priority) return -1;
    if (!a.priority && b.priority) return 1;
    if (a.save > b.save) return -1;
    if (a.save < b.save) return 1;
    if (a.item < b.item) return -1;
    if (a.item > b.item) return 1;
  });
}

function displayFlyerData(data) {
  for (var i = 0; i < data.length; i++) {
    var o = data[i];
    console.log((o.priority ? 'Y' : '') + '\t' + o.item + "\t" + o.price + "\t" + o.save + "\t" + o.details);
  }
}

getFlyer(flyerURL).then(prioritizeFlyer).then(displayFlyerData);

I’ll check next week to see if the accessible flyer URL changes each time, or if I can determine the correct publication ID by going to a stable URL. Anyway, this was fun to write!

Scan ~/bin and turn the scripts into Emacs commands

I want to automate little things on my computer so that I don’t have to look up command lines or stitch together different applications. Many of these things make sense to turn into shell scripts. That way, I can call them from other programs and assign keyboard shortcuts to them. Still, I spend most of my computer time in Emacs, and I don’t want to think about whether I’ve defined a command in Emacs Lisp or in a shell script. Besides, I like the way Helm lets me type parts of commands in order to select and call them.

Emacs Lisp allows you to define a macro that results in Emacs Lisp code. In this case, I want to define interactive functions so I can call them with M-x. In case I decide to call them from Emacs Lisp, such as (my/shell/rotate-screen "left"), I want to be able to pass arguments. I’m also using dash.el to provide functions like -filter and -not, although I could rewrite this to just use the standard Emacs Lisp functions.

Here’s the code that scans a given directory for executable files and creates interactive functions, and some code that calls it for my ~/bin directory.

(defmacro my/convert-shell-scripts-to-interactive-commands (directory)
  "Make the shell scripts in DIRECTORY available as interactive commands."
  (cons 'progn
          (-map
           (lambda (filename)
             (let ((function-name (intern (concat "my/shell/" (file-name-nondirectory filename)))))
               `(defun ,function-name (&rest args)
                  (interactive)
                  (apply 'call-process ,filename nil nil nil args))))
           (-filter (-not #'file-directory-p)
                    (-filter #'file-executable-p (directory-files directory t))))))

(my/convert-shell-scripts-to-interactive-commands "~/bin")

Let’s see how that goes!

2015-12-14 Emacs News

Links from reddit.com/r/emacs, /r/orgmode, Hacker News, planet.emacsen.org, Youtube, the Emacs commit log, the changes to the Emacs NEWS file, and emacs-devel.

Past Emacs News round-ups

2015-12-10 Emacs Chat: John Wiegley on maintaining Emacs and how you can help

These are the bugs that have the “easy” keyword. Note that some of them are because of the package or mode name. =)

John Wiegley shared how he uses Gnus and Org to help him with the volume of Emacs-related information, and how people can get started with Emacs development.

  • 0:02 Gnus for mail and news
  • 0:04 Organizing groups by topic
  • 0:05 Adaptive scoring and prioritization
  • 0:09 Setup for mail: Gmail, Fetchmail, Dovecot, Gnus
  • 0:11 Time: 1-2 hours a day
  • 0:13 Community-building
  • 0:15 Using Org to keep track of initiatives
  • 0:19 Reading bug reports in Gnus
  • 0:22 How people can help: tests, documentation, reviewing bugs
  • 0:24 Coverage
  • 0:33 Efficiency, benchmarks
  • 0:40 Magit, Projectile, Flycheck
  • 0:45 Following up on emacs-devel topics: IDEs, APIs, lexical binding, Guile, etc.

You can e-mail John Wiegley at [email protected]. The emacs-devel mailing list is at https://lists.gnu.org/mailman/listinfo/emacs-devel.

Event page on Google+
Ogg Vorbis (audio only)
MP3 (audio only)

View the full blog post for the transcript. Thanks to Phil Hudson for volunteering to transcribe this!

[Read more →]

Scripting and the Toronto Public Library’s movie collection

We hardly ever watch movies in the theatre now, since we prefer watching movies with subtitles and the ability to pause. Fortunately, the Toronto Public Library has a frequently updated collection of DVDs. The best time to grab a movie is when it’s a new release, since DVDs that have been in heavy circulation can get pretty scratched up from use. However, newly released movies can’t be reserved. You need to find them at the library branches they’re assigned to, and then you can borrow them for seven days. You can check the status of each movie online to see if it’s in the library or when it’s due to be returned.

Since there are quite a few movies on our watch list, quite a few library branches we can walk to, and some time flexibility as to when to go, checking all those combinations is tedious. I wrote a script that takes a list of branches and a list of movie URLs, checks the status of each, and displays a table sorted by availability and location. My code gives me a list like this:

In Library Annette Street Mad Max Fury Road M 10-8:30 T 12:30-8:30 W 10-6 Th 12:30-8:30 F 10-6 Sat 9-5
In Library Bloor/Gladstone Inside Out M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5 Sun 1:30-5
In Library Bloor/Gladstone Match M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5 Sun 1:30-5
In Library Jane/Dundas Avengers: Age of Ultron M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5
In Library Jane/Dundas Ant-Man M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5
In Library Jane/Dundas Mad Max Fury Road M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5
In Library Jane/Dundas Minions M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5
In Library Perth/Dupont Chappie T 12:30-8:30 W 10-6 Th 12:30-8:30 F 10-6 Sat 9-5
In Library Runnymede Ant-Man M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5
In Library Runnymede Minions M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5
In Library St. Clair/Silverthorn Kingsman: the Secret Service T 12:30-8:30 W 10-6 Th 12:30-8:30 F 10-6 Sat 9-5
In Library St. Clair/Silverthorn Mad Max Fury Road T 12:30-8:30 W 10-6 Th 12:30-8:30 F 10-6 Sat 9-5
In Library Swansea Memorial Ant-Man T 10-6 W 1-8 Th 10-6 Sat 10-5
In Library Swansea Memorial Chappie T 10-6 W 1-8 Th 10-6 Sat 10-5
In Library Swansea Memorial Kingsman: the Secret Service T 10-6 W 1-8 Th 10-6 Sat 10-5
In Library Swansea Memorial Kingsman: the Secret Service T 10-6 W 1-8 Th 10-6 Sat 10-5
In Library Swansea Memorial Mad Max Fury Road T 10-6 W 1-8 Th 10-6 Sat 10-5
In Library Swansea Memorial Minions T 10-6 W 1-8 Th 10-6 Sat 10-5
2015-12-08 Perth/Dupont Terminator Genisys T 12:30-8:30 W 10-6 Th 12:30-8:30 F 10-6 Sat 9-5
2015-12-08 Perth/Dupont Mad Max Fury Road T 12:30-8:30 W 10-6 Th 12:30-8:30 F 10-6 Sat 9-5
2015-12-09 Swansea Memorial Avengers: Age of Ultron T 10-6 W 1-8 Th 10-6 Sat 10-5

… many more rows omitted. =)

With this data, I can decide that Swansea Memorial has a bunch of things I might want to check out, and pick that as the destination for my walk. Sure, there’s a chance that someone else might check out the movies before I get there (although I can minimize that by getting to the library as soon as it opens), or that the video has been misfiled or misplaced, but overall, the system tends to work fine.

It’s easy for me to send the output to myself by email, too. I just select the part of the table I care about and use Emacs’ M-x shell-command-on-region (M-|) to mail it to myself with the command mail -s "Videos to check out" [email protected].

The first time I ran my script, I ended up going to Perth/Dupont to pick up seven movies in addition to the two I picked up from Annette Library. Many of the movies had been returned but not yet shelved, so the librarian retrieved them from his bin and gave them to me. When I got back, W- looked at the stack of DVDs by the television and said, “You know that’s around 18 hours of viewing, right?” It’ll be fine for background watching. =)

Little things like this make me glad that I can write scripts and other tiny tools to make my life better. Anything that involves multiple steps or combining information from multiple sources might be simpler with a script. I wrote this script as a command-line tool with NodeJS, since I’m comfortable with the HTML request and parsing libraries available there.

Anyway, here’s the code, in case you want to build on the idea. Have fun!

/* Shows you which videos are available at which libraries.

   Input: A json filename, which should be a hash of the form:
   {"branches": {"Branch name": "Additional branch details (ex: hours)", ...},
   "videos": [{"Title": "URL to library page"}, ...]}.

   Example: {
   "branches": {
   "Runnymede": "M 9-8:30 T 9-8:30 W 9-8:30 Th 9-8:30 F 9-5 Sat 9-5"
   },
   "videos": [
   {"title": "Avengers: Age of Ultron", "url": "http://www.torontopubliclibrary.ca/detail.jsp?Entt=RDM3350205&R=3350205"}
   ]}

   Output:
   Status,Branch,Title,Branch notes
*/

var rp = require('request-promise');
var moment = require('moment');
var async = require('async');
var cheerio = require('cheerio');
var q = require('q');
var csv = require('fast-csv');
var fs = require('fs');

if (process.argv.length < 3) {
  console.log('Please specify the JSON file to read the branches and videos from.');
  process.exit(1);
}

var config = JSON.parse(fs.readFileSync(process.argv[2]));
var branches = config.branches;
var videos = config.videos;

/*
  Returns a promise that will resolve with an array of [status,
  branch, movie, info], where status is either the next due date, "In
  Library", etc. */
function checkStatus(branches, movie) {
  var url = movie.url;
  var matches = url.match(/R=([0-9]+)/);
  return rp.get(
    'http://www.torontopubliclibrary.ca/components/elem_bib-branch-holdings.jspf?print=&numberCopies=1&itemId='
      + matches[1]).then(function(a) {
        var $ = cheerio.load(a);
        var results = [];
        var lastBranch = '';              
        $('tr.notranslate').each(function() {
          var row = $(this);
          var cells = row.find('td');
          var branch = $(cells[0]).text().replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
          var due = $(cells[2]).text().replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
          var status = $(cells[3]).text().replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
          if (branch) { lastBranch = branch; }
          else { branch = lastBranch; }
          if (branches[branch]) {
            if (status == 'On loan' && (matches = due.match(/Due: (.*)/))) {
              status = moment(matches[1], 'DD/MM/YYYY').format('YYYY-MM-DD');
            }
            if (status != 'Not Available - Search in Progress') {
              results.push([status, branch, movie.title, branches[branch]]);
            }
          }
        });
        return results;
      });
}

function checkAllVideos(branches, videos) {
  var results = [];
  var p = q.defer();
  async.eachLimit(videos, 5, function(video, callback) {
    checkStatus(branches, video).then(function(result) {
      results = results.concat(result);
      callback();
    });
  }, function(err) {
    p.resolve(results.sort(function(a, b) {
      if (a[0] == 'In Library') {
        if (b[0] == 'In Library') {
          if (a[1] < b[1]) return -1;
          if (a[1] > b[1]) return 1;
          if (a[2] < b[2]) return -1;
          if (a[2] > b[2]) return 1;
          return 0;
        } else {
          return -1;
        }
      }
      if (b[0] == 'In Library') { return 1; }
      if (a[0] < b[0]) { return -1; }
      if (a[0] > b[0]) { return 1; }
      return 0;
    }));
  });
  return p.promise;
}

checkAllVideos(branches, videos).then(function(result) {
  csv.writeToString(result, {}, function(err, data) {
    console.log(data);
  });
});

P.S. Okay, I’m really tempted to walk over to Swansea Memorial, but W- reminds me that we’ve got a lot of movies already waiting to be watched. So I’ll probably just walk to the supermarket, but I’m looking forward to running this script once we get through our backlog of videos!