<?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 - nodejs</title>
	<subtitle>Emacs, sketches, and life</subtitle>
	<link rel="self" type="application/atom+xml" href="https://sachachua.com/blog/tag/nodejs/feed/atom/index.xml" />
  <link rel="alternate" type="text/html" href="https://sachachua.com/blog/tag/nodejs" />
  <id>https://sachachua.com/blog/tag/nodejs/feed/atom/index.xml</id>
  <generator uri="https://11ty.dev">11ty</generator>
	<updated>2016-02-21T02:19:00Z</updated>
<entry>
		<title type="html">Listing random npmjs.com packages updated today</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2016/02/listing-random-npmjs-com-packages-updated-today/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2016-02-21T07:20:58Z</updated>
    <published>2016-02-21T02:19:00Z</published>
    <category term="geek" />
		<id>https://sachachua.com/blog/?p=28625</id>
		<content type="html"><![CDATA[<p>I was looking for a way to randomly learn about packages hosted at npmjs.com so that I can come across libraries I might not have thought of searching for. The <a href="https://docs.npmjs.com/misc/registry">registry data</a> is available at <a href="https://registry.npmjs.org/">https://registry.npmjs.org/</a>, and there's a public CouchDB mirror at <a href="https://skimdb.npmjs.com/registry">https://skimdb.npmjs.com/registry</a> . Someday, when I know more about CouchDB, I might be able to query it and do other things.</p>
<p>In the meantime, this <a href="https://github.com/npm/npm-registry-couchapp/issues/242">Github issue</a> pointed me to a view of <a href="https://registry.npmjs.org/-/all/static/today.json">all packages modified today</a>, which is a good-enough proxy for what I'm interested in.</p>
<p>Here's an AngularJS app that displays the list and highlights a random item.</p>
<p><a href="https://sachachua.com/blog/wp-content/uploads/2016/02/Screenshot_2016-02-20_21-09-21.png" rel="attachment wp-att-28626"><img loading="lazy" class="alignnone size-medium wp-image-28626" src="https://sachachua.com/blog/wp-content/uploads/2016/02/Screenshot_2016-02-20_21-09-21-640x235.png" alt="Screenshot_2016-02-20_21-09-21" width="640" height="235" srcset="https://sachachua.com/blog/wp-content/uploads/2016/02/Screenshot_2016-02-20_21-09-21-640x235.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/02/Screenshot_2016-02-20_21-09-21-280x103.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/02/Screenshot_2016-02-20_21-09-21-768x282.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/02/Screenshot_2016-02-20_21-09-21.png 1322w" sizes="(max-width: 640px) 100vw, 640px"></a></p>
<div class="org-src-container">
<pre class="src src-html">&lt;<span class="org-function-name">html</span> <span class="org-variable-name">ng-app</span>=<span class="org-string">"myApp"</span>&gt;
  &lt;<span class="org-function-name">head</span>&gt;
    &lt;<span class="org-function-name">script</span> <span class="org-variable-name">type</span>=<span class="org-string">"text/javascript"</span>
      <span class="org-variable-name">src</span>=<span class="org-string">"https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0/angular.min.js"</span>&gt;&lt;/<span class="org-function-name">script</span>&gt;
    &lt;<span class="org-function-name">script</span>&gt;
     // https://registry.npmjs.org/-/all/static/today.json
     // from https://github.com/npm/npm-registry-couchapp/issues/242
     var app = angular.module('myApp', []);
     app.controller('npmTodayCtrl', function($scope, $http) {
       $scope.randomize = function() {
         $scope.random = $scope.packages[Math.floor(Math.random() * $scope.packages.length)];
       }
       $http.get('https://registry.npmjs.org/-/all/static/today.json').then(function(info) {
         $scope.packages = info.data;
         $scope.randomize();
       });
     });
    &lt;/<span class="org-function-name">script</span>&gt;
  &lt;/<span class="org-function-name">head</span>&gt;
  &lt;<span class="org-function-name">body</span> <span class="org-variable-name">ng-controller</span>=<span class="org-string">"npmTodayCtrl"</span>&gt;
    &lt;<span class="org-function-name">div</span>&gt;&lt;<span class="org-function-name">a</span> <span class="org-variable-name">href</span>=<span class="org-string">""</span> <span class="org-variable-name">ng-click</span>=<span class="org-string">"randomize()"</span>&gt;Random highlight:&lt;/<span class="org-function-name">a</span>&gt;&lt;/<span class="org-function-name">div</span>&gt;
    &lt;<span class="org-function-name">div</span> <span class="org-variable-name">ng-if</span>=<span class="org-string">"random"</span> <span class="org-variable-name">style</span>=<span class="org-string">"margin-top: 1em; font-size: x-large"</span>&gt;
      &lt;<span class="org-function-name">strong</span>&gt;&lt;<span class="org-function-name">a</span> <span class="org-variable-name">ng-href</span>=<span class="org-string">"https://npmjs.com/package/\{\{random.name\}\}"</span>&gt;\{\{random.name\}\}&lt;/<span class="org-function-name">a</span>&gt;&lt;/<span class="org-function-name">strong</span>&gt;
      \{\{random.description\}\}
    &lt;/<span class="org-function-name">div</span>&gt;
    &lt;<span class="org-function-name">hr</span>&gt;
    &lt;<span class="org-function-name">table</span>&gt;
      &lt;<span class="org-function-name">tr</span> <span class="org-variable-name">ng-repeat</span>=<span class="org-string">"package in packages"</span>&gt;
        &lt;<span class="org-function-name">td</span>&gt;&lt;<span class="org-function-name">a</span> <span class="org-variable-name">ng-href</span>=<span class="org-string">"https://npmjs.com/package/\{\{package.name\}\}"</span>&gt;\{\{package.name\}\}&lt;/<span class="org-function-name">a</span>&gt;&lt;/<span class="org-function-name">td</span>&gt;
        &lt;<span class="org-function-name">td</span>&gt;\{\{package.description\}\}&lt;/<span class="org-function-name">td</span>&gt;
      &lt;/<span class="org-function-name">tr</span>&gt;
    &lt;/<span class="org-function-name">table</span>&gt;
  &lt;/<span class="org-function-name">body</span>&gt;
&lt;/<span class="org-function-name">html</span>&gt;
</pre>
</div>
<p>You can <a href="https://sachachua.com/blog/2016/02/listing-random-npmjs-com-packages-updated-today/#comment">view 1 comment</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2016%2F02%2Flisting-random-npmjs-com-packages-updated-today%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><entry>
		<title type="html">Building a simple sketch navigator for myself</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2016/01/building-simple-sketch-navigator/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2019-06-12T18:58:05Z</updated>
    <published>2016-01-22T22:28:00Z</published>
    <category term="drawing" />
<category term="geek" />
<category term="organization" />
		<id>https://sachachua.com/blog/?p=28573</id>
		<content type="html"><![CDATA[<p>In 2015, I built the habit of <a href="https://sachachua.com/blog/2015/01/drawing-thoughts-index-cards/">drawing daily index-card-sized thoughts</a>. Some days, I reached (or blew past!) my target of five index cards a day. Other days, I backfilled my daily journal entries based on fuzzy memories and my time logs. In February 2015, I started using a <i>yyyy-mm-dd&lt;letter&gt;</i> naming convention so that I could easily refer to individual cards. For a while, I tried to be fairly disciplined about organizing sketches into outlines and building up chunks into blog posts. That fell by the wayside when I found it difficult to concentrate, but I kept drawing anyway. I saved the images to my hard drive, started tagging them with keywords in the filenames, and uploaded my sketches to Flickr as a way to back up and share my files.</p>
<p>I've recently been exploring ways to take advantage of the Samsung tablet that W- bought some time ago. It works wonderfully as a second screen that I can use to review an old sketch while I draw another one. The <a href="https://play.google.com/store/apps/details?id=com.snapwood.flickfolio&amp;hl=en">FlickFolio</a> app lets me do a random slideshow of the past 2000 images in my photostream or in an album. This turns out to be an excellent way to jog my memory and prompt me to revisit questions or decisions.</p>
<p><a href="https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-22d-What-do-I-want-from-my-idea-pipeline-index-card-zettelkasten-ideas-thinking-questions-index-cards.png" rel="attachment wp-att-28574"><img loading="lazy" class="alignnone size-medium wp-image-28574" src="https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-22d-What-do-I-want-from-my-idea-pipeline-index-card-zettelkasten-ideas-thinking-questions-index-cards-640x383.png" alt="2016-01-22d What do I want from my idea pipeline &#45;&#45; index card #zettelkasten #ideas #thinking #questions #index-cards" width="640" height="383" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-22d-What-do-I-want-from-my-idea-pipeline-index-card-zettelkasten-ideas-thinking-questions-index-cards-640x383.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-22d-What-do-I-want-from-my-idea-pipeline-index-card-zettelkasten-ideas-thinking-questions-index-cards-280x168.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-22d-What-do-I-want-from-my-idea-pipeline-index-card-zettelkasten-ideas-thinking-questions-index-cards-768x460.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-22d-What-do-I-want-from-my-idea-pipeline-index-card-zettelkasten-ideas-thinking-questions-index-cards.png 1497w" sizes="(max-width: 640px) 100vw, 640px"></a></p>
<p>2016-01-22d What do I want from my idea pipeline – index card #zettelkasten #ideas #thinking #questions #index-cards.png</p>
<p>Now those fragmented thoughts are starting to pay off. Many of my old index cards are on topics I haven't thought about in a while. It's fun to see what I've learned in the meantime, or to follow up on things I've forgotten. It's like I'd been filling an idea pipeline or seeding an idea garden. I get this steady stream of questions, decisions, observations, memories, ideas, and notes, and I can build on those prompts instead of having to start from scratch.</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28575" src="https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18g-How-do-I-want-my-sketches-to-help-me-think-index-card-drawing-sketches-zettelkasten-notes-ref-2015-11-18a-640x383.png" alt="2016-01-18g How do I want my sketches to help me think &#45;&#45; index card #drawing #sketches #zettelkasten #notes ref 2015-11-18a" width="640" height="383" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18g-How-do-I-want-my-sketches-to-help-me-think-index-card-drawing-sketches-zettelkasten-notes-ref-2015-11-18a-640x383.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18g-How-do-I-want-my-sketches-to-help-me-think-index-card-drawing-sketches-zettelkasten-notes-ref-2015-11-18a-280x168.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18g-How-do-I-want-my-sketches-to-help-me-think-index-card-drawing-sketches-zettelkasten-notes-ref-2015-11-18a-768x460.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18g-How-do-I-want-my-sketches-to-help-me-think-index-card-drawing-sketches-zettelkasten-notes-ref-2015-11-18a.png 1497w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p>2016-01-18g How do I want my sketches to help me think – index card #drawing #sketches #zettelkasten #notes ref 2015-11-18a.png</p>
<p>I want these index cards to help me shuffle ideas and possibly see serendipitous combinations. They're small, so capturing thoughts on them doesn't require as much effort as, say, writing a blog post. They're a good way to smooth out and organize thoughts, and I can chunk up those thoughts into longer posts. The sketches are easy to review, too, so they work well as digital footprints. I re-read the year's blog posts as part of my annual review and sometimes I reread my posts on a monthly basis as well, but it's not as immediate or as wide-ranging as flipping through a random selection of images.</p>
<p>A random slideshow is a good start, but I've been meaning to make a sketch browser that's a little more tuned to what I want.</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28576" src="https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18d-What-do-I-want-in-a-sketch-browser-index-card-drawing-coding-plan-640x383.png" alt="2016-01-18d What do I want in a sketch browser &#45;&#45; index card #drawing #coding #plan" width="640" height="383" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18d-What-do-I-want-in-a-sketch-browser-index-card-drawing-coding-plan-640x383.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18d-What-do-I-want-in-a-sketch-browser-index-card-drawing-coding-plan-280x168.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18d-What-do-I-want-in-a-sketch-browser-index-card-drawing-coding-plan-768x460.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/01/2016-01-18d-What-do-I-want-in-a-sketch-browser-index-card-drawing-coding-plan.png 1497w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p>2016-01-18d What do I want in a sketch browser – index card #drawing #coding #plan.png</p>
<p>I wrote a simple NodeJS server that I can run on my laptop and access from the tablet (or other devices) while I'm on my home network. At first, I was trying to figure out an interface that would let me navigate by month/week/day, but then I realized that something simpler might be a more useful way to begin.</p>
<p>I started off by making it display random sketches:</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28583" src="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-34-640x360.png" alt="Screenshot_2016-01-22_17-16-34" width="640" height="360" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-34-640x360.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-34-280x157.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-34-768x432.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-34.png 1366w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p>Then I extended it to let me browse by tag:</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28577" src="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-19-33-640x256.png" alt="Screenshot_2016-01-22_17-19-33" width="640" height="256" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-19-33-640x256.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-19-33-280x112.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-19-33-768x307.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-19-33.png 1348w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p>And then to see a list of tags by frequency:</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28581" src="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-12-640x370.png" alt="Screenshot_2016-01-22_17-17-12" width="640" height="370" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-12-640x370.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-12-280x162.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-12.png 644w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p>Or by alphabetic order, since that might be handier if I'm trying to look for something specific on a mobile device</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28580" src="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-23.png" alt="Screenshot_2016-01-22_17-17-23" width="634" height="417" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-23.png 634w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-23-280x184.png 280w" sizes="(max-width: 634px) 100vw, 634px"></p>
<p>I like distinguishing between daily/weekly/monthly/yearly reviews and non-journal sketches, too:</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28582" src="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-43-640x360.png" alt="Screenshot_2016-01-22_17-16-43" width="640" height="360" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-43-640x360.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-43-280x157.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-43-768x432.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-16-43.png 1366w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28578" src="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-18-04-640x270.png" alt="Screenshot_2016-01-22_17-18-04" width="640" height="270" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-18-04-640x270.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-18-04-280x118.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-18-04.png 737w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p><img loading="lazy" class="alignnone size-medium wp-image-28579" src="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-34-640x179.png" alt="Screenshot_2016-01-22_17-17-34" width="640" height="179" srcset="https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-34-640x179.png 640w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-34-280x78.png 280w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-34-768x214.png 768w, https://sachachua.com/blog/wp-content/uploads/2016/01/Screenshot_2016-01-22_17-17-34.png 1207w" sizes="(max-width: 640px) 100vw, 640px"></p>
<p>It's nice to be able to build custom little tools like that. =)</p>
<p><a href="https://github.com/sachac/scripts/blob/master/serve-sketches.coffee">Here's the script on Github,</a> in case you're curious or you want to build on the idea.</p>
<p>You can <a href="https://sachachua.com/blog/2016/01/building-simple-sketch-navigator/#comment">view 1 comment</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2016%2F01%2Fbuilding-simple-sketch-navigator%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><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>