<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/atom.xsl" type="text/xsl"?><feed
	xmlns="http://www.w3.org/2005/Atom"
	xmlns:thr="http://purl.org/syndication/thread/1.0"
	xml:lang="en-US"
	><title>Sacha Chua - tag - tech-and-home</title>
	<subtitle>Emacs, sketches, and life</subtitle>
	<link rel="self" type="application/atom+xml" href="https://sachachua.com/blog/tag/tech-and-home/feed/atom/index.xml" />
  <link rel="alternate" type="text/html" href="https://sachachua.com/blog/tag/tech-and-home" />
  <id>https://sachachua.com/blog/tag/tech-and-home/feed/atom/index.xml</id>
  <generator uri="https://11ty.dev">11ty</generator>
	<updated>2015-12-18T17:21:00Z</updated>
<entry>
		<title type="html">Scripting and the grocery store flyer</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2015/12/scripting-grocery-store-flyer/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2015-12-18T22:22:44Z</updated>
    <published>2015-12-18T17:21:00Z</published>
    <category term="geek" />
		<id>https://sachachua.com/blog/?p=28518</id>
		<content type="html"><![CDATA[<p>We plan the week&#8217;s meals around the grocery store flyers, taking advantage of what&#8217;s on sale (chicken, ground beef, etc.) and stocking up when the opportunity presents itself (for example, diced tomatoes or cream of mushroom soup).</p>
<p>The flyers are usually delivered on Thursdays. We do most of our grocery shopping at No Frills because it&#8217;s convenient and almost always a good price, but sometimes we&#8217;ll go to Freshco or Metro if there&#8217;s a particularly good sale. I might flip through the other flyers if we&#8217;re looking for something in particular, but most of the time, we just toss them out. I&#8217;d love to opt out of paper flyers, but that doesn&#8217;t seem to be an option in our neighbourhood.</p>
<p>It doesn&#8217;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:</p>
<table border="2" frame="hsides" rules="groups" cellspacing="0" cellpadding="6">
<colgroup>
<col class="org-left">
<col class="org-left">
<col class="org-right">
<col class="org-right">
<col class="org-left"> </colgroup>
<tbody>
<tr>
<td class="org-left">Y</td>
<td class="org-left">Clementines</td>
<td class="org-right">2.47</td>
<td class="org-right"></td>
<td class="org-left">2 lb bag product of Spain $2.47</td>
</tr>
<tr>
<td class="org-left">Y</td>
<td class="org-left">Smithfield Bacon</td>
<td class="org-right">3.97</td>
<td class="org-right"></td>
<td class="org-left">500 g selected varieties $3.97</td>
</tr>
<tr>
<td class="org-left">Y</td>
<td class="org-left">Thomas&#8217; Cinnamon Raisin Bread</td>
<td class="org-right">2.5</td>
<td class="org-right"></td>
<td class="org-left">675 g or Weston Kaisers 12&#8217;s selected varieties $5.00 or $2.50 ea.</td>
</tr>
<tr>
<td class="org-left">Y</td>
<td class="org-left">Unico Tomatoes</td>
<td class="org-right">0.97</td>
<td class="org-right"></td>
<td class="org-left">796 mL or Beans 540 mL selected varieties $0.97</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Fresh Boneless Skinless Chicken Breast</td>
<td class="org-right">3.33</td>
<td class="org-right">2.78</td>
<td class="org-left">BIG Pack!™ DECEMBER 18TH &#8211; 24TH ONLY! $3.33 lb/$7.34/kg save $2.78/lb</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Purex</td>
<td class="org-right">3.97</td>
<td class="org-right">2.02</td>
<td class="org-left">2.03 L $3.97 save $2.02</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Frozen Steelhead Trout Fillets</td>
<td class="org-right">5.97</td>
<td class="org-right">2.0</td>
<td class="org-left">filets de truite $5.97 lb/$13.16/kg save $2.00/lb</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Heinz Tomato Juice</td>
<td class="org-right">0.97</td>
<td class="org-right">1.52</td>
<td class="org-left">1.36 L selected varieties $0.97 save $1.52</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Nestlé Multi-Pack Chocolate or Bagged Chocolate</td>
<td class="org-right">2.88</td>
<td class="org-right">0.61</td>
<td class="org-left">45-246 g selected varieties $2.88 save 61¢</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Source</td>
<td class="org-right">4.97</td>
<td class="org-right">0.5</td>
<td class="org-left">16 x 100 g selected varieties $4.97 save 50 ¢</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Franco Gravy</td>
<td class="org-right">0.67</td>
<td class="org-right">0.32</td>
<td class="org-left">284 mL selected varieties $0.67 save 32¢</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Ocean Spray Cranberry Sauce</td>
<td class="org-right">1.67</td>
<td class="org-right">0.32</td>
<td class="org-left">348 mL whole or jellied $1.67 save 32¢</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">10 lb Bag Yellow Potatoes, 5 lb Bag Carrots or 10 lb Bag Yellow Cooking Onions</td>
<td class="org-right">1.87</td>
<td class="org-right"></td>
<td class="org-left">product of Ontario, Canada no. 1 grade or 5 lb Bag Rutabaga product of Canada, no. 1 grade $1.87</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Betty Crocker Hamburger Helper</td>
<td class="org-right">1.5</td>
<td class="org-right"></td>
<td class="org-left">158-255 g or Mashed or Scallop Potatoes 141-215 g selected varieties $3.00 or $1.50 ea.</td>
</tr>
<tr>
<td class="org-left"></td>
<td class="org-left">Blackberries</td>
<td class="org-right">1.25</td>
<td class="org-right"></td>
<td class="org-left">6 oz product of U.S.A. or Mexico DECEMBER 18TH &#8211; 24TH ONLY! 4/$5.00 or $1.25 ea.</td>
</tr>
</tbody>
</table>
<p>(more lines omitted)</p>
<p>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&#8217;ll add more things to the priority list, and the script will get smarter and smarter.</p>
<p>I can use Org commands to move the rows up or down or remove the rows I&#8217;m not interested in. Then I can take the second column of the script&#8217;s output with Emacs&#8217; <code>copy-rectangle-as-kill</code> command (<code>C-x r M-w</code>), and paste it into <a href="http://ourgroceries.com/">OurGroceries</a>&#8216; import dialog. That builds a shopping list that&#8217;s sorted by the aisles I&#8217;ve previously set up, and this list is synchronized with our phones.</p>
<p>I&#8217;ve added the script to <a href="https://github.com/sachac/scripts/blob/master/check-grocery-flyer.js">https://github.com/sachac/scripts/blob/master/check-grocery-flyer.js</a>, 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&#8217;t included in the repository, but you can probably come up with your own if you want to adapt the idea. =)</p>
<p>Here&#8217;s the source, just in case:</p>
<div class="org-src-container">
<pre class="src src-js2">#!/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]+)( |&amp;nbsp;)+(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]|&amp;nbsp;)+/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 &lt; data.length; i++) {
    var name = data[i].item.toLowerCase();
    for (var j = 0; j &lt; staples.length; j++) {
      if (name.match(staples[j])) {
        data[i].priority = true;
      }
    }
  }
  return data.sort(function(a, b) {
    if (a.priority &amp;&amp; !b.priority) return -1;
    if (!a.priority &amp;&amp; b.priority) return 1;
    if (a.save &gt; b.save) return -1;
    if (a.save &lt; b.save) return 1;
    if (a.item &lt; b.item) return -1;
    if (a.item &gt; b.item) return 1;
  });
}

function displayFlyerData(data) {
  for (var i = 0; i &lt; 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);
</pre>
</div>
<p>I&#8217;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!</p>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2015%2F12%2Fscripting-grocery-store-flyer%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry>
</feed>