<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Pepijn Looije</title>
    <description>Pepijn Looije looks like a random string of characters but it's actually my name</description>
    <link>https://p.epij.nl/</link>
    <atom:link href="https://p.epij.nl/feed.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>Real World Ledger part 1: Weighing Eggs in Baskets</title>
        <description>&lt;p&gt;Do you ever feel like you’re losing grip on your personal finances? You deposit
into your savings account in one currency, buy stocks and bonds in another, and
maybe even hodl on to some cryptocurrency. You keep a finger on the pulse and
occasionally check your assets’ value, but volatile prices and exchange rates
make it challenging. Ledger is a command-line accounting tool that addresses
these issues. In this post I’ll introduce you to it.&lt;/p&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;The world around you is changing: interest rates on savings accounts slide to
0%, stock markets bubble and crash, and global political debate
intensifies. Above all, your bank or government expects you to pay back that
student loan sometime in the future. It is safe to say that our economy is a
rough sea.&lt;/p&gt;

&lt;p&gt;This post is part one in a series where I show how to map and plan your
financial positions in Ledger so you’re able to navigate those real-world issues
with confidence. How to do that exactly isn’t trivial, though. With this blog
series I hope to fill that skills gap—the same that I encountered when
starting with Ledger. I found many online examples too abstract and missing
human/societal context for someone unfamiliar with accounting.&lt;/p&gt;

&lt;p&gt;That said, I won’t be explaining to you the theory behind techniques like
double-entry accounting and investment portfolios in this post because I am
neither an accountant nor am I an investment advisor. The Ledger documentation
does a good job of explaining double-entry&lt;sup&gt;&lt;a id=&quot;fnr.1&quot; class=&quot;footref&quot; href=&quot;#fn.1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; and Investopedia
explains portfolios well&lt;sup&gt;&lt;a id=&quot;fnr.2&quot; class=&quot;footref&quot; href=&quot;#fn.2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. I will limit myself to the
narrative and practical examples—those are the things I’ve personally
experienced.&lt;/p&gt;

&lt;h2 id=&quot;part-1-weighing-eggs-in-baskets&quot;&gt;Part 1: Weighing Eggs in Baskets&lt;/h2&gt;

&lt;p&gt;The example story through this blog series features the development of a basic
financial situation into an investment portfolio with carefully weighted stocks,
government bonds, cryptocurrency, and of course cash. All is fine and balanced
until a cryptocurrency hype comes knocking at the door and exposes the portfolio
manager, you, to a novel risk.&lt;/p&gt;

&lt;p&gt;We get started in this post by diversifying our savings into stocks, bonds, and
cryptocurrency. This way, we won’t have all our eggs in one basket. We also
discuss how you convert different assets to one currency so we can reliably
weigh them: apples to apples. The weighing is essential when creating your
investment portfolio &lt;strong&gt;allocation&lt;/strong&gt;—which we’ll discuss in the next post.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Ledger is a powerful, double-entry accounting system that is accessed from the
UNIX command-line.” — &lt;a href=&quot;https://www.ledger-cli.org&quot;&gt;https://www.ledger-cli.org&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To follow along with the story below, you will need a terminal with Ledger
installed and a plain text file editor, such as Sublime Text. If you use macOS,
installing Ledger is easy using &lt;a href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt;: &lt;code class=&quot;highlighter-rouge&quot;&gt;brew install ledger&lt;/code&gt;. Feel free to
make your workflow more pleasant by installing a Ledger mode in your text
editor—this gives you syntax highlighting. Sublime and other well-known
editors (like Emacs and vim) have Ledger modes readily available online.&lt;/p&gt;

&lt;h3 id=&quot;the-basics-a-single-currency-and-a-single-asset-class&quot;&gt;The basics: a single currency and a single asset class&lt;/h3&gt;

&lt;h4 id=&quot;our-first-posting-adding-our-savings-account&quot;&gt;Our first posting: adding our savings account&lt;/h4&gt;

&lt;p&gt;Let’s start simple: we have a savings account at a bank called ASN
bank&lt;sup&gt;&lt;a id=&quot;fnr.3&quot; class=&quot;footref&quot; href=&quot;#fn.3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; in our home country where most of our money resides. This
account already has money in it—obviously we don’t start owning assets the
moment we begin using Ledger—so we have to initialize our balance by moving
money from &lt;em&gt;somewhere&lt;/em&gt;. Idiomatically that &lt;em&gt;somewhere&lt;/em&gt; is an account called
‘opening balances’. When we express this in Ledger, this is what the file
&lt;code class=&quot;highlighter-rouge&quot;&gt;postings1.dat&lt;/code&gt; (&lt;code class=&quot;highlighter-rouge&quot;&gt;.dat&lt;/code&gt; is commonly used with Ledger, but feel free to use
something else like &lt;code class=&quot;highlighter-rouge&quot;&gt;.txt&lt;/code&gt;) looks like:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ledger&quot; data-lang=&quot;ledger&quot;&gt;2018-01-01 Opening Balances
    Assets:NL:ASN:Savings                 € 1,337.00
    Equity:Opening Balances              € -1,337.00&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;How do we interpret these three lines? Every posting has a date (&lt;code class=&quot;highlighter-rouge&quot;&gt;2018-01-01&lt;/code&gt;)
and a payee (&lt;code class=&quot;highlighter-rouge&quot;&gt;Opening Balances&lt;/code&gt;). Then, what follows directly beneath it are the
entries belonging to that posting. In this case, we move the &lt;code class=&quot;highlighter-rouge&quot;&gt;€ 1337&lt;/code&gt; from
‘opening balances’ to the savings account. Most of the labels here are arbitrary
and depend on your preference and taste. I like to structure actual bank
accounts as follows: country, name of bank, type of account. That results in
&lt;code class=&quot;highlighter-rouge&quot;&gt;Assets:NL:ASN:Savings&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now we run our first query using the Ledger command-line tool. We ask for the
&lt;code class=&quot;highlighter-rouge&quot;&gt;balance&lt;/code&gt; of accounts that match &lt;code class=&quot;highlighter-rouge&quot;&gt;assets&lt;/code&gt; in the file &lt;code class=&quot;highlighter-rouge&quot;&gt;postings.dat&lt;/code&gt;.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ledger &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; postings1.dat balance assets&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The result, as expected, the balance of one asset account:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;€ 1,337.00  Assets:NL:ASN:Savings&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;our-first-mutation-interest-from-savings-and-deposits-and-withdrawals&quot;&gt;Our first mutation: interest from savings, and deposits and withdrawals&lt;/h4&gt;

&lt;p&gt;Fast forward 6 months. We have received some interest from the bank and did a
couple of deposits and withdrawals. We could add postings for all the deposits
and withdrawals, but that’s a lot of premature work and definitely not the
required to benefit from Ledger. That’s why we’re using an ‘adjustment’ account
in the following &lt;strong&gt;addition&lt;/strong&gt; to our &lt;code class=&quot;highlighter-rouge&quot;&gt;postings1.dat&lt;/code&gt; file, calling it
&lt;code class=&quot;highlighter-rouge&quot;&gt;postings2.dat&lt;/code&gt;.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ledger&quot; data-lang=&quot;ledger&quot;&gt;2018-06-01 ASN
    Assets:NL:ASN:Savings                 € 3,787.50
    Income:Interest                         € -42
    Equity:Adjustment&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;A net amount of € 3,787.50 was added to the savings account, of which € 42 was
interest received on the principal. The rest was the result of deposits and
withdrawals. We don’t really care about tracking all those transactions in
detail right now, so we lazily use an adjustment account. Lastly, we’re able to
omit the amount of &lt;code class=&quot;highlighter-rouge&quot;&gt;Equity:Adjustment&lt;/code&gt; because there’s only one possibility: &lt;code class=&quot;highlighter-rouge&quot;&gt;€
-3,787.50 - € 42 = € -3,745.5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The adjustment account resolves a common discouragement of adopting Ledger that
I keep hearing—people think that Ledger requires them to arduously type in all
transactions like a monkey. You don’t, and above all, you can always do that
later or build scripts to do it for you should you so desire.&lt;/p&gt;

&lt;p&gt;We now rerun the Ledger command-line tool. This time, we ask for the &lt;code class=&quot;highlighter-rouge&quot;&gt;balance&lt;/code&gt;
of all accounts, not just assets:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ledger &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; postings2.dat balance&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Please note that the total of all accounts always sums to zero—that condition
is the main property of double-entry accounting:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;          € 5,124.50  Assets:NL:ASN:Savings
         € -5,082.50  Equity
         € -3,745.50    Adjustment
         € -1,337.00    Opening Balances
            € -42.00  Income:Interest
--------------------
                   0&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;going-deeper-multiple-currencies-and-asset-classes&quot;&gt;Going deeper: multiple currencies and asset classes&lt;/h3&gt;

&lt;h4 id=&quot;diversifying-into-multiple-assets&quot;&gt;Diversifying into multiple assets&lt;/h4&gt;

&lt;p&gt;We decided to diversify, hoping to get a better return than the ~0% interest
rate on your savings account&lt;sup&gt;&lt;a id=&quot;fnr.4&quot; class=&quot;footref&quot; href=&quot;#fn.4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; in our ~2% inflation
habitat&lt;sup&gt;&lt;a id=&quot;fnr.5&quot; class=&quot;footref&quot; href=&quot;#fn.5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;. But, at the same time, you don’t want to go all-in on
stocks because it’s generally considered a bad idea to put all your eggs in one
basket. That’s why we diversify and buy some government bonds and cryptocurrency
too. ‘Interactive Brokers’ and ‘Binck Bank’ in the file below are examples of
stock/bond brokers. &lt;code class=&quot;highlighter-rouge&quot;&gt;postings3.dat&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ledger&quot; data-lang=&quot;ledger&quot;&gt;2018-07-01 Interactive Brokers
    Assets:NL:ASN:Savings                   € -1,285
    Assets:US:Interactive Brokers:Cash       $ 1,500

2018-07-02 Binck Bank
    Assets:NL:ASN:Savings                   € -2,000
    Assets:NL:BinckBank:Cash

2018-07-03 Interactive Brokers
    Assets:US:Interactive Brokers:Stocks      6 AAPL @ $ 183.92
    Assets:US:Interactive Brokers:Cash

2018-07-04 Binck Bank
    Assets:NL:BinckBank:Bonds      1,100 &quot;NL2014-47&quot; @ € 1.39
    Assets:NL:BinckBank:Stocks                5 HEIA @ € 86.08
    Assets:NL:BinckBank:Cash

2018-07-05 Coinbase
    Assets:Cryptocurrency:BTC wallet         BTC 0.1
    Assets:NL:ASN:Savings                     € -561&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In the example above we use different syntax to reach the same goal: buying one
commodity by selling another commodity (such as stocks from US dollars and
Bitcoin from euros). The Ledger docs explain the differences
clearly&lt;sup&gt;&lt;a id=&quot;fnr.6&quot; class=&quot;footref&quot; href=&quot;#fn.6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;Let’s check the impact of our asset diversification buying spree on our balance:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ledger &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; postings3.dat balance assets &lt;span class=&quot;nt&quot;&gt;--no-total&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--flat&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Please be advised that I passed two new arguments: &lt;code class=&quot;highlighter-rouge&quot;&gt;--no-total&lt;/code&gt; and
&lt;code class=&quot;highlighter-rouge&quot;&gt;--flat&lt;/code&gt;. The total is superfluous because we’re only looking at
assets. Conversely, the total is valuable when you’re looking at both assets and
liabilities. Subtracting them yields net worth&lt;sup&gt;&lt;a id=&quot;fnr.7&quot; class=&quot;footref&quot; href=&quot;#fn.7&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;. And &lt;code class=&quot;highlighter-rouge&quot;&gt;--flat&lt;/code&gt; is
purely aesthetic. It suppresses Ledger’s automatic hierarchy view because it is
confusing when printing heterogenous commodities (such as currencies, stocks,
etc.).&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;        BTC 0.1  Assets:Cryptocurrency:BTC wallet
     € 1,278.50  Assets:NL:ASN:Savings
1,100 NL2014-47  Assets:NL:BinckBank:Bonds
        € 40.60  Assets:NL:BinckBank:Cash
         5 HEIA  Assets:NL:BinckBank:Stocks
          $ 396  Assets:US:Interactive Brokers:Cash
         6 AAPL  Assets:US:Interactive Brokers:Stocks&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This balance sheet matches our expectations but it isn’t giving us much extra
information about each of the assets relative to each other—value-wise we’re
comparing apples to oranges. Wouldn’t it be nice to have all the assets
converted to one currency so we can compare apples to apples?&lt;/p&gt;

&lt;h4 id=&quot;implicit-and-explicit-market-prices&quot;&gt;Implicit and explicit market prices&lt;/h4&gt;

&lt;p&gt;In order to compare values of assets we have to pick a base currency to convert
them to. I’m carrying a Dutch passport so my usual pick is to convert everything
to euros. But, as long as you supply Ledger the exchange rates, you could
express the value of your assets, even your guitar if you’re so inclined, in
whatever commodity you like—from Apple stock to real
apples&lt;sup&gt;&lt;a id=&quot;fnr.8&quot; class=&quot;footref&quot; href=&quot;#fn.8&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;. Obviously your tools shouldn’t stop you from expressing the
value of your guitar in apples that you pick from the tree! The only thing
Ledger needs is either an &lt;em&gt;implicit&lt;/em&gt; or &lt;em&gt;explicit&lt;/em&gt; market price.&lt;/p&gt;

&lt;p&gt;We’ll discuss prices in a moment. Before, to see the value of our assets
expressed in euros, we run the following command (adding &lt;code class=&quot;highlighter-rouge&quot;&gt;--exchange €&lt;/code&gt;):&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ledger &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; postings3.dat b Assets &lt;span class=&quot;nt&quot;&gt;--exchange&lt;/span&gt; € &lt;span class=&quot;nt&quot;&gt;--no-total&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Finally, we have a birds-eye view of all our assets’s value across different
countries, accounts, and currencies:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;€ 5,124.50  Assets
  € 561.00    Cryptocurrency:BTC wallet
€ 3,278.50    NL
€ 1,278.50      ASN:Savings
€ 2,000.00      BinckBank
€ 1,529.00        Bonds
   € 40.60        Cash
  € 430.40        Stocks
€ 1,285.00    US:Interactive Brokers
  € 339.65      Cash
  € 945.35      Stocks&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;How did Ledger convert everything to euros? Ledger keeps track of prices
&lt;em&gt;implicitly&lt;/em&gt; and also allows you to specify prices
manually—&lt;em&gt;explicitly&lt;/em&gt;. Let’s focus on the implicit part first, by asking
Ledger for the prices that it stored so far:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ledger &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; postings3.dat prices&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;With this command you peek into Ledger’s internal price database. The prices
that you see were established by the postings in &lt;code class=&quot;highlighter-rouge&quot;&gt;postings3.dat&lt;/code&gt; and are all
&lt;em&gt;implicit&lt;/em&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;2018/07/01 €        $ 1.167315175097
2018/07/03 AAPL         $ 183.92
2018/07/04 &quot;NL2014-47&quot;       € 1.39
2018/07/04 HEIA          € 86.08
2018/07/05 BTC        € 5,610.00&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As a matter of experiment, let’s say the price of Apple stock recently shot
up. It rose to an extent that we’re now curious to see how much the value of our
US brokerage account increased. To find out, we’re going to &lt;em&gt;explicitly&lt;/em&gt; express
Apple’s stock price in US dollars in a new file called &lt;code class=&quot;highlighter-rouge&quot;&gt;prices.dat&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ledger&quot; data-lang=&quot;ledger&quot;&gt;P 2018-08-03 AAPL $ 207.99&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The single line in this file states: on &lt;code class=&quot;highlighter-rouge&quot;&gt;2018-08-03&lt;/code&gt; the price for &lt;code class=&quot;highlighter-rouge&quot;&gt;AAPL&lt;/code&gt; in &lt;code class=&quot;highlighter-rouge&quot;&gt;$&lt;/code&gt;
was &lt;code class=&quot;highlighter-rouge&quot;&gt;207.99&lt;/code&gt;. Let’s make this file available to Ledger by specifying
&lt;code class=&quot;highlighter-rouge&quot;&gt;--price-db&lt;/code&gt; and querying assets in the US (in which Apple belongs) only
(&lt;code class=&quot;highlighter-rouge&quot;&gt;Assets:US&lt;/code&gt;):&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ledger &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; postings3.dat &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
       balance Assets:US &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
       &lt;span class=&quot;nt&quot;&gt;--exchange&lt;/span&gt; € &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
       &lt;span class=&quot;nt&quot;&gt;--price-db&lt;/span&gt; prices.dat &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
       &lt;span class=&quot;nt&quot;&gt;--no-total&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Indeed, we see the gains on Apple stock reflected by our increased total US
assets value. Apple stock got converted to US dollars got converted to euros:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;€ 1,408.72  Assets:US:Interactive Brokers
  € 339.65    Cash
€ 1,069.07    Stocks&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You should add a line to &lt;code class=&quot;highlighter-rouge&quot;&gt;prices.dat&lt;/code&gt; for every price that you want to track. I
personally have more than a thousand lines in my prices file and retrieve some
prices automatically using APIs (predominantly forex rates). The benefit of a
high resolution like that is that graphical plots of my assets, liabilities, and
net worth (using a daily interval on the x-axis) are less jumpy.&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;To summarize, we’ve just created our first postings, discovered the implicit
exchange rates that Ledger keeps and added an Apple stock price explicitly. All
along the way we were able to query our balance in two representations: in its
original commodity and converted to one base currency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In part 2 we’ll look at how you materialize an investment portfolio strategy
and asset allocation using Ledger.&lt;/strong&gt; Please leave your email address if you want
a notification once it’s published! I’d also love to hear your feedback about
this post and hear suggestions about topics that you’d like to see discussed in
depth. Reach out to me on Twitter: &lt;a href=&quot;https://twitter.com/ppnlo&quot;&gt;@ppnlo&lt;/a&gt;. Or through email: replace the first
dot in the domain name with an @.&lt;/p&gt;

&lt;!-- Begin MailChimp Signup Form --&gt;
&lt;link href=&quot;//cdn-images.mailchimp.com/embedcode/classic-10_7.css&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot; /&gt;

&lt;style type=&quot;text/css&quot;&gt;
	#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; }
	/* Add your own MailChimp form style overrides in your site stylesheet or in this style block.
	   We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
&lt;/style&gt;

&lt;div id=&quot;mc_embed_signup&quot;&gt;
&lt;form action=&quot;https://epij.us19.list-manage.com/subscribe/post?u=359e0c2277a83d3411e823493&amp;amp;id=dad6148db5&quot; method=&quot;post&quot; id=&quot;mc-embedded-subscribe-form&quot; name=&quot;mc-embedded-subscribe-form&quot; class=&quot;validate&quot; target=&quot;_blank&quot; novalidate=&quot;&quot;&gt;
    &lt;div id=&quot;mc_embed_signup_scroll&quot;&gt;
	&lt;h2&gt;Feel free to subscribe: I'll notify you when publish the next part&lt;/h2&gt;
&lt;div class=&quot;indicates-required&quot;&gt;&lt;span class=&quot;asterisk&quot;&gt;*&lt;/span&gt; indicates required&lt;/div&gt;
&lt;div class=&quot;mc-field-group&quot;&gt;
	&lt;label for=&quot;mce-EMAIL&quot;&gt;Email Address  &lt;span class=&quot;asterisk&quot;&gt;*&lt;/span&gt;
&lt;/label&gt;
	&lt;input type=&quot;email&quot; value=&quot;&quot; name=&quot;EMAIL&quot; class=&quot;required email&quot; id=&quot;mce-EMAIL&quot; /&gt;
&lt;/div&gt;
	&lt;div id=&quot;mce-responses&quot; class=&quot;clear&quot;&gt;
		&lt;div class=&quot;response&quot; id=&quot;mce-error-response&quot; style=&quot;display:none&quot;&gt;&lt;/div&gt;
		&lt;div class=&quot;response&quot; id=&quot;mce-success-response&quot; style=&quot;display:none&quot;&gt;&lt;/div&gt;
	&lt;/div&gt;    &lt;!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups--&gt;
    &lt;div style=&quot;position: absolute; left: -5000px;&quot; aria-hidden=&quot;true&quot;&gt;&lt;input type=&quot;text&quot; name=&quot;b_359e0c2277a83d3411e823493_dad6148db5&quot; tabindex=&quot;-1&quot; value=&quot;&quot; /&gt;&lt;/div&gt;
    &lt;div class=&quot;clear&quot;&gt;&lt;input type=&quot;submit&quot; value=&quot;Subscribe&quot; name=&quot;subscribe&quot; id=&quot;mc-embedded-subscribe&quot; class=&quot;button&quot; /&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/form&gt;
&lt;/div&gt;
&lt;script type=&quot;text/javascript&quot; src=&quot;//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;(function($) {window.fnames = new Array(); window.ftypes = new Array();fnames[0]='EMAIL';ftypes[0]='email';fnames[1]='FNAME';ftypes[1]='text';fnames[2]='LNAME';ftypes[2]='text';fnames[3]='ADDRESS';ftypes[3]='address';fnames[4]='PHONE';ftypes[4]='phone';}(jQuery));var $mcj = jQuery.noConflict(true);&lt;/script&gt;

&lt;!--End mc_embed_signup--&gt;

&lt;h2 id=&quot;appendix&quot;&gt;Appendix&lt;/h2&gt;

&lt;p&gt;As always, this post is written in a literate
programming&lt;sup&gt;&lt;a id=&quot;fnr.9&quot; class=&quot;footref&quot; href=&quot;#fn.9&quot;&gt;9&lt;/a&gt;&lt;/sup&gt; style, which means that the code samples in
it are reproducible and correct. Check out the Org-mode and Babel source code on
GitHub: &lt;a href=&quot;https://raw.githubusercontent.com/pepijn/pepijn.github.io/master/org/real-world-ledger-part-1.org&quot;&gt;real-world-ledger-part-1.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you Thomas Smolders, Pieter Levels, Arend Koopmans, Rik Helwegen and Nils
Mackay for helping me with this post!&lt;/em&gt;&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.1&quot; href=&quot;#fnr.1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://www.ledger-cli.org/3.0/doc/ledger3.html&quot;&gt;https://www.ledger-cli.org/3.0/doc/ledger3.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.2&quot; href=&quot;#fnr.2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://www.investopedia.com/terms/p/portfolio.asp&quot;&gt;https://www.investopedia.com/terms/p/portfolio.asp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.3&quot; href=&quot;#fnr.3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://eerlijkegeldwijzer.nl/bankwijzer/banken/asn-bank/&quot;&gt;Eerlijke Bankwijzer: ASN Bank&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.4&quot; href=&quot;#fnr.4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; Interest rates for ABN Amro savings accounts, similar
to other Dutch banks: &lt;a href=&quot;https://www.abnamro.nl/en/personal/savings/spaarrente.html&quot;&gt;https://www.abnamro.nl/en/personal/savings/spaarrente.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.5&quot; href=&quot;#fnr.5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;http://statline.cbs.nl/StatWeb/publication/?VW=T&amp;amp;DM=SLNL&amp;amp;PA=70936NED&amp;amp;D1=0&amp;amp;D2=(l-34)-l&amp;amp;HD=081020-1258&amp;amp;HDR=T&amp;amp;STB=G1&quot;&gt;CBS inflation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.6&quot; href=&quot;#fnr.6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://www.ledger-cli.org/3.0/doc/ledger3.html#Explicit-posting-costs&quot;&gt;https://www.ledger-cli.org/3.0/doc/ledger3.html#Explicit-posting-costs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.7&quot; href=&quot;#fnr.7&quot;&gt;7&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://en.wikipedia.org/wiki/Net_worth&quot;&gt;https://en.wikipedia.org/wiki/Net_worth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.8&quot; href=&quot;#fnr.8&quot;&gt;8&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://www.ledger-cli.org/3.0/doc/ledger3.html#Posting-costs&quot;&gt;https://www.ledger-cli.org/3.0/doc/ledger3.html#Posting-costs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;&lt;a id=&quot;fn.9&quot; href=&quot;#fnr.9&quot;&gt;9&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://en.wikipedia.org/wiki/Literate_programming&quot;&gt;https://en.wikipedia.org/wiki/Literate_programming&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Thu, 23 Aug 2018 00:00:00 -0300</pubDate>
        <link>https://p.epij.nl/ledger-cli/accounting/2018/08/23/real-world-ledger-part-1/</link>
        <guid isPermaLink="true">https://p.epij.nl/ledger-cli/accounting/2018/08/23/real-world-ledger-part-1/</guid>
      </item>
    
      <item>
        <title>Sidestepping Heroku's limits with PostgreSQL 9.5's UPSERT</title>
        <description>&lt;p&gt;My roommate needed help with building a web scraper. As a result of various
constraints, we ended up using Heroku. There was, however, a database row limit
that we had to work around. It turned out to be a nice opportunity to play with
the new PostgreSQL 9.5 &lt;code class=&quot;highlighter-rouge&quot;&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt;, also known as UPSERT,
functionality. The goal of this article is to introduce UPSERT to you via a
practical example.&lt;/p&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;My roommate, an aspiring programmer, recently asked me for advice on how to
build a web scraper. The scraper should run on a machine that is constanly
connected to the internet, because it should check at least every hour or
so. Running the scraper on his laptop is not an option for my roommate, because
he frequently takes it with him on the road. Neither is running it on a remote
server, because he possesses none nor is he planning to spend money on one. We
tried out Heroku; a free platform as a service provider that embraces all of
these constraints.&lt;/p&gt;

&lt;h3 id=&quot;choosing-the-right-storage&quot;&gt;Choosing the right storage&lt;/h3&gt;

&lt;p&gt;The &lt;a href=&quot;https://elements.heroku.com/addons/scheduler&quot;&gt;Heroku Scheduler addon&lt;/a&gt; enabled us to run a script every 10 minutes (the
highest available frequency) that scrapes the targeted website. With that
requirement settled, we needed a way to compare the old state to the new state,
so we could send ourselves an email as soon as the website changed. The simplest
solution we could come up with involved saving the hashed HTML contents into a
Heroku config variable. That solves the problem of notification, but will not
allow us analyse the data later. For this reason, using a database made
sense. PostgreSQL storage is cheap, free actually on Heroku, so we picked that
one.&lt;/p&gt;

&lt;h3 id=&quot;heroku-limitations&quot;&gt;Heroku limitations&lt;/h3&gt;

&lt;p&gt;Normally, I would insert one row per scraping run with the scraped contents (or
a foreign key to a unique piece of content) and a timestamp into the
database. In this case, however, it was not a good idea because the free Heroku
PostgreSQL database offering has certain limitations. You are allowed to have a
maximum of &lt;a href=&quot;https://elements.heroku.com/addons/heroku-postgresql&quot;&gt;10.000 rows&lt;/a&gt; in the database. A scraper that scrapes every 10 minutes
reaches the limit after 40 days of service. We needed a way to circumvent the
limitation, since we were planning on scraping longer than that.&lt;/p&gt;

&lt;p&gt;What if we scaled horizontally instead of vertically? Instead of adding rows, we
could grow columns and consequently circumvent the row limit. One row per unique
piece of scraped content with an ever expanding array of timestamps. PostgreSQL
arrays seem to be a good fit for this. One could implement this using two
queries. First, a &lt;code class=&quot;highlighter-rouge&quot;&gt;SELECT&lt;/code&gt; establishes whether we have seen the content
before. Second, an &lt;code class=&quot;highlighter-rouge&quot;&gt;INSERT&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt; respectively inserts a new piece of
content with the current time to the array or updates an existing row by
appending the current time. Or, you use the new &lt;a href=&quot;https://wiki.postgresql.org/wiki/What's_new_in_PostgreSQL_9.5#INSERT_..._ON_CONFLICT_DO_NOTHING.2FUPDATE_.28.22UPSERT.22.29&quot;&gt;INSERT … ON CONFLICT DO UPDATE&lt;/a&gt;
operation instead, like I will show you in this article.&lt;/p&gt;

&lt;h2 id=&quot;methods&quot;&gt;Methods&lt;/h2&gt;

&lt;p&gt;In this article, I will only show you how to complete the task using UPSERT. If
you would like to see how you could achieve similar behavior with PostgreSQL
versions below 9.5, please check out &lt;a href=&quot;http://stackoverflow.com/a/17267423&quot;&gt;this Stackoverflow answer&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;the-query&quot;&gt;The query&lt;/h3&gt;

&lt;p&gt;We built the plumbing of the scraper using Python; the &lt;a href=&quot;https://github.com/pepijn/python-upsert-scraper&quot;&gt;source code is available
on GitHub&lt;/a&gt;. The Python script executes the following query–the interesting
part–every time it runs.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IF&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EXISTS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scraps&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;serial&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;hash&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bytea&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timestamptz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- This extension yields the digest function that enables us to&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- hash the body and index it efficiently. Moreover, PostgreSQL&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- does not allow indexes on very large text columns. We expect to&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- store large HTML bodies so we definitely need the hashing.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EXTENSION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IF&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EXISTS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pgcrypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;


&lt;span class=&quot;c1&quot;&gt;-- Capture the script input (body) via a CTE (the WITH part) so we&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- can use it multiple times in the query. Once to save the body,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- once to hash it.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- This is exactly the INSERT you would write without UPSERT.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scraps&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'sha1'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARRAY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timestamptz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- Here it gets interesting; we utilize the UNIQUE index on hash&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- to yield a conflict if the body already exists. If that&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- happens, we append the new seen_at (via the special 'EXCLUDED'&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- table) to the seen_at array.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CONFLICT&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DO&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scraps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EXCLUDED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- The query returns a summary of the row so I can use it in my&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- blog post. This part will run, but the output is ignored, in&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- production.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;RETURNING&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'...'&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;the-test&quot;&gt;The test&lt;/h3&gt;

&lt;p&gt;We supply the query above to the script below via the &lt;code class=&quot;highlighter-rouge&quot;&gt;$QUERY&lt;/code&gt; variable. First,
we initialize an empty database and run the query twice with the &lt;code class=&quot;highlighter-rouge&quot;&gt;body&lt;/code&gt; variable
set to &lt;code class=&quot;highlighter-rouge&quot;&gt;content&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;ts&lt;/code&gt; set to the SQL function &lt;code class=&quot;highlighter-rouge&quot;&gt;now()&lt;/code&gt;. After that, we
simulate a change on the targeted website by running the query again after
setting &lt;code class=&quot;highlighter-rouge&quot;&gt;content&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;changed&lt;/code&gt;. In production, the &lt;code class=&quot;highlighter-rouge&quot;&gt;body&lt;/code&gt; variable contains an
HTML document. The output of the script is printed below it.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;dropdb &lt;span class=&quot;nt&quot;&gt;--if-exists&lt;/span&gt; scraper_test
createdb scraper_test

psql scraper_test &lt;span class=&quot;nt&quot;&gt;--variable&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;'content'&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
		  &lt;span class=&quot;nt&quot;&gt;--variable&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'now()'&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
		  &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$QUERY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$QUERY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;nContent changed...&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;n

psql scraper_test &lt;span class=&quot;nt&quot;&gt;--variable&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;'changed'&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
		  &lt;span class=&quot;nt&quot;&gt;--variable&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'now()'&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
		  &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$QUERY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
EOF&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;CREATE TABLE
CREATE EXTENSION
 id |  hash  |  body   |             seen_at
----+--------+---------+----------------------------------
  1 | \x0... | content | {&quot;2016-04-20 11:05:31.38508+02&quot;}
(1 row)

INSERT 0 1
CREATE TABLE
CREATE EXTENSION
 id |  hash  |  body   |                             seen_at
----+--------+---------+------------------------------------------------------------------
  1 | \x0... | content | {&quot;2016-04-20 11:05:31.38508+02&quot;,&quot;2016-04-20 11:05:31.386758+02&quot;}
(1 row)

INSERT 0 1

Content changed...

CREATE TABLE
CREATE EXTENSION
 id |  hash  |  body   |              seen_at
----+--------+---------+-----------------------------------
  3 | \x3... | changed | {&quot;2016-04-20 11:05:31.397775+02&quot;}
(1 row)

INSERT 0 1&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;

&lt;p&gt;The scraper has been running flawlessly in production for almost two
weeks. Let’s see how many rows have been added during that time.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scraps&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;24&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;After running every 10 minutes for almost two weeks, the scraper inserted just
24 records. Before we inspect the contents of the database, let’s make sure that
we really are in compliance with the Heroku PostgreSQL maximum rows limitation:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/heroku_stats.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We have to &lt;a href=&quot;http://www.postgresql.org/docs/9.5/static/functions-array.html#ARRAY-FUNCTIONS-TABLE&quot;&gt;unnest&lt;/a&gt; the &lt;code class=&quot;highlighter-rouge&quot;&gt;seen_at&lt;/code&gt; array to obtain the total count of scraper
runs.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unnest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scraps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;un&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;2282&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Let’s break the 2282 rows down by date and aggregate the count of checks and
changes of content that occured that day. Please check out the appendix for the
exact query that I used.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;    day     | checks | changes
------------+--------+---------
 2016-04-04 |     87 |       0
 2016-04-05 |    144 |       1
 2016-04-06 |    144 |       0
 2016-04-07 |    139 |       3
 2016-04-08 |    144 |       4
 2016-04-09 |    144 |       0
 2016-04-10 |    144 |       0
 2016-04-11 |    141 |       2
 2016-04-12 |    144 |       4
 2016-04-13 |    144 |       1
 2016-04-14 |    138 |       3
 2016-04-15 |    139 |       0
 2016-04-16 |    144 |       0
 2016-04-17 |    144 |       0
 2016-04-18 |    143 |       4
 2016-04-19 |    144 |       2
 2016-04-20 |     55 |       0
 Total:     |   2282 |      24&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The sums of checks and changes match the unnested and total counts above,
respectively. Note: the amount of checks differs between days probably because
of the following:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Scheduler is a best-effort service. There is no guarantee that jobs will execute
at their scheduled time, or at all. Scheduler has a known issue whereby
scheduled processes are occasionally skipped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source: &lt;a href=&quot;https://devcenter.heroku.com/articles/scheduler#known-issues-and-alternatives&quot;&gt;Known issues and alternatives; Heroku Scheduler documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;discussion&quot;&gt;Discussion&lt;/h2&gt;

&lt;p&gt;This scraping method only works when the website is static and the content
changes slowly, compared to dynamic websites with different HTML output on each
request. For example, some websites return a different &lt;a href=&quot;https://en.wikipedia.org/wiki/Cross-site_request_forgery&quot;&gt;XSRF token&lt;/a&gt; at every
visit. In that case, every scraping run inserts a new row into the database,
negating the savings of our UPSERT horizontal expansion.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;The HTML output of our targeted website only changed when the content of
interest changed, leading to our high checks vs. changes ratio. Using UPSERT for
scraping turned out to be a good fit for this website because it enables us to
scrape for some time ahead, while logging all of the captured data.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://twitter.com/share&quot; class=&quot;twitter-share-button&quot; data-via=&quot;ppnlo&quot; data-size=&quot;large&quot;&gt;Tweet&lt;/a&gt;
&lt;script&gt;!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');&lt;/script&gt;&lt;/p&gt;

&lt;h2 id=&quot;acknowledgements&quot;&gt;Acknowledgements&lt;/h2&gt;

&lt;p&gt;Thank you people that made &lt;a href=&quot;http://orgmode.org/&quot;&gt;Org mode&lt;/a&gt; and &lt;a href=&quot;http://orgmode.org/worg/org-contrib/babel/&quot;&gt;Babel&lt;/a&gt;. :-)&lt;/p&gt;

&lt;h2 id=&quot;appendix&quot;&gt;Appendix&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/pepijn/pepijn.github.io/blob/master/org/upsert-scraper.org&quot;&gt;source code of this article&lt;/a&gt; is available online.&lt;/p&gt;

&lt;h3 id=&quot;breakdown-query&quot;&gt;Breakdown query&lt;/h3&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;date_trunc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'day'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;checks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;changes&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unnest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seen_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scraps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;un&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;UNION&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Total:'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;checks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;changes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;day&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;software-used&quot;&gt;Software used&lt;/h3&gt;

&lt;h4 id=&quot;postgresql&quot;&gt;PostgreSQL&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;psql postgres &lt;span class=&quot;nt&quot;&gt;--tuples-only&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'SELECT version()'&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;PostgreSQL 9.5.2 on x86_64-apple-darwin15.4.0, compiled by Apple LLVM version 7.3.0 (clang-703.0.29), 64-bit&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;psql&quot;&gt;psql&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;psql &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;psql (PostgreSQL) 9.5.2&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

</description>
        <pubDate>Wed, 20 Apr 2016 00:00:00 -0300</pubDate>
        <link>https://p.epij.nl/postgresql/heroku/2016/04/20/sidestepping-heroku-limits-with-postgresql-upsert/</link>
        <guid isPermaLink="true">https://p.epij.nl/postgresql/heroku/2016/04/20/sidestepping-heroku-limits-with-postgresql-upsert/</guid>
      </item>
    
      <item>
        <title>Visualizing 24 hours of medical students cramming anatomy</title>
        <description>&lt;p&gt;tl;dr: in the first year of medical school, I built an application that helps
fellow students and myself with studying anatomy. The answers of the last exam,
submitted by students while revising, have been visualized with &lt;a href=&quot;https://github.com/acaudwell/Gource&quot;&gt;Gource&lt;/a&gt;. Please
check out the &lt;a href=&quot;https://youtu.be/xytCT8QoSDU&quot;&gt;YouTube video&lt;/a&gt; for the result:
&lt;a href=&quot;https://youtu.be/xytCT8QoSDU&quot;&gt;&lt;img src=&quot;/images/anatomy_visualization_screenshot.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;introduction-to-the-application&quot;&gt;Introduction to the application&lt;/h2&gt;

&lt;p&gt;For medical students it is inevitable: you have to know all the anatomical terms
by heart. The task is easy, but the amount of structures one has to learn is
quite intimidating. I remember the feelings of despair that arose while staring
at the latin words in the anatomy book. After one brave attempt, my attention
span had decided: we needed a better way to study this (one that involves
computers).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/anatomy_google_analytics.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;building-the-visualization&quot;&gt;Building the visualization&lt;/h2&gt;

&lt;p&gt;This is the &lt;code class=&quot;highlighter-rouge&quot;&gt;exam_date&lt;/code&gt; that we will be using for this visualization:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;2015-09-21 08:30:00 +02:00&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;retrieving-the-answers&quot;&gt;Retrieving the answers&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/images/anatomy_visualization_answers_legend.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We use one big PostgreSQL query that yields all the answers from the timeframe
in the right format. No scripting needed!&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;c1&quot;&gt;-- We use trigram similarity to determine answer correctness&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EXTENSION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IF&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EXISTS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_trgm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;translated_categories&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;CASE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Bovenarm'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Upper arm'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Bovenbeen'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Upper leg'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Elleboog'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Elbow'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Enkel'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Ankle'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Hals'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Neck'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Heupgewricht'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Hip joint'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Kniegewricht'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Knee joint'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Onderarm'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Lower arm'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Onderbeen'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Lower leg'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Pols'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Wrist'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Schouder'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Shoulder'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Voet'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Foot'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;ELSE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;END&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;categories&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;answer_colors&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;similarity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;CASE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00FF00'&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;-- Green: 100% correct answer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'32FF00'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'65FF00'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'99FF00'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'CCFF00'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FFFF00'&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;-- Yellow: meh&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FFCC00'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FF9900'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FF6600'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FF3200'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FF0000'&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;-- Red: wrong answer :-(&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;END&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;generate_series&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ranked_sessions&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OVER&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_rank&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;BETWEEN&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exam_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'28 hours'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exam_date&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;plate_numbers&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;category_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OVER&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PARTITION&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;category_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plates&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;extract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;session_rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;-- Green 'beam' (A) when the answer is 100% correct&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;CASE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;similarity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;structures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'A'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;ELSE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'M'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;END&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;concat_ws&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'/'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;translated_categories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;plate_numbers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;structures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OVER&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PARTITION&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;structure_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;structures&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;structures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;structure_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answer_colors&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answer_colors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;similarity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
     &lt;span class=&quot;n&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;similarity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;structures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;numeric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plate_numbers&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plate_numbers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plate_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ranked_sessions&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rs&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;translated_categories&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;translated_categories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;category_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;BETWEEN&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;zone&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exam_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'28 hours'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;zone&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exam_date&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/acaudwell/Gource/wiki/Custom-Log-Format&quot;&gt;&lt;img src=&quot;/images/gource_custom_log_format_docs.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The results from the query seem to match Gource’s custom log format:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 3 &lt;span class=&quot;nv&quot;&gt;$ANSWERS_PATH&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo
tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 3 &lt;span class=&quot;nv&quot;&gt;$ANSWERS_PATH&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;1442729360|1|A|Knee joint/1/meniscus medialis/1|00FF00
1442729371|1|A|Knee joint/1/lig. cruciatum posterior/1|00FF00
1442729377|1|A|Knee joint/1/meniscus lateralis/1|00FF00

1442815272|271|M|Upper leg/4/m. biceps femoris caput longum/75|32FF00
1442815283|271|A|Upper leg/4/m. vastus lateralis/75|00FF00
1442815322|271|M|Upper leg/4/m. peroneus longus/71|FF3200&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;How many answers do we have in total?&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; &amp;lt; &lt;span class=&quot;nv&quot;&gt;$ANSWERS_PATH&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;50687&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;captions&quot;&gt;Captions&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/acaudwell/Gource/wiki/Captions&quot;&gt;&lt;img src=&quot;/images/gource_captions_docs.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;nb&quot;&gt;tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 3 &lt;span class=&quot;nv&quot;&gt;$CAPTIONS_PATH&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;1442809800|2 hours until exam
1442813400|1 hour until exam
1442817000|Exam begins at 08:30...&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;user-images&quot;&gt;User images&lt;/h3&gt;

&lt;h4 id=&quot;retrieving-user-agent-data-per-session-rank&quot;&gt;Retrieving user agent data per session (rank)&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OVER&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;user_agent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;first_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_start&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;answers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;BETWEEN&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;zone&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exam_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'28 hours'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;zone&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exam_date&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_agent&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;1|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12|1465360|2015-09-20 06:09:19.603637
2|Mozilla/5.0 (Windows NT 10.0; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0|1465384|2015-09-20 06:19:55.221907
3|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/600.8.9 (KHTML, like Gecko) Version/8.0.8 Safari/600.8.9|1465408|2015-09-20 06:28:14.890441&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;linking-the-sessions-to-browser-icons&quot;&gt;Linking the sessions to browser icons&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/acaudwell/Gource&quot;&gt;&lt;img src=&quot;/images/gource_user_images_docs.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$USER_IMAGES_PATH&lt;/span&gt;/&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;1,2,3&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;.png | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;/ &lt;span class=&quot;nt&quot;&gt;-f4-&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;1.png -&amp;gt; /Users/pepijn/Desktop/browser_icons/Safari.png
2.png -&amp;gt; /Users/pepijn/Desktop/browser_icons/Firefox.png
3.png -&amp;gt; /Users/pepijn/Desktop/browser_icons/Safari.png&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;/h3&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;gource &lt;span class=&quot;nt&quot;&gt;-1280x720&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--bloom-intensity&lt;/span&gt; 0.7 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--caption-duration&lt;/span&gt; 15 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--caption-file&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$CAPTIONS_PATH&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--caption-size&lt;/span&gt; 50 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--dir-colour&lt;/span&gt; 00FFFF &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--dir-name-depth&lt;/span&gt; 2 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--file-idle-time&lt;/span&gt; 10 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--hide&lt;/span&gt; filenames &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--highlight-dirs&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--max-file-lag&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--seconds-per-day&lt;/span&gt; 10000 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--stop-at-end&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--title&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Answers from AMC/UvA (Amsterdam) 3rd year medical students revising online the day before their orthopaedics (course 3.1) anatomy exam'&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nt&quot;&gt;--user-image-dir&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$USER_IMAGES_PATH&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	     &lt;span class=&quot;nv&quot;&gt;$ANSWERS_PATH&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
     2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://twitter.com/share&quot; class=&quot;twitter-share-button&quot; data-via=&quot;ppnlo&quot;&gt;Tweet&lt;/a&gt; &lt;script&gt;!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');&lt;/script&gt;&lt;/p&gt;

</description>
        <pubDate>Tue, 13 Oct 2015 00:00:00 -0300</pubDate>
        <link>https://p.epij.nl/medicine/university/visualization/gource/postgresql/2015/10/13/visualizing-24-hours-of-medical-students-cramming-anatomy/</link>
        <guid isPermaLink="true">https://p.epij.nl/medicine/university/visualization/gource/postgresql/2015/10/13/visualizing-24-hours-of-medical-students-cramming-anatomy/</guid>
      </item>
    
      <item>
        <title>Building a GeoJSON travel log: an introduction to Org mode and Babel</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Literate_programming&quot;&gt;Literate programming&lt;/a&gt; is a
technique that caught my attention after recently stumbling upon Howard Abrams’
&lt;a href=&quot;https://www.youtube.com/watch?v=dljNabciEGg&quot;&gt;‘Literate Devops with Emacs’ video&lt;/a&gt;. The
intersection of watching this awesome video, reading about
&lt;a href=&quot;https://help.github.com/articles/mapping-geojson-files-on-github/&quot;&gt;GeoJSON rendering on GitHub&lt;/a&gt;
and returning from a road trip last summer led me to building my own travel
log. In my first blog post I would like to show you how it works.
&lt;a href=&quot;https://github.com/pepijn/travel_log/blob/master/my_summer_2015.geojson&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/pepijn/travel_log/03c34c500a0251dbbaa2430eb7a643de2b4ab6f0/media/geojson_github_2.png&quot; alt=&quot;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Creating a travel log means entering a lot of data like dates and locations; a
job that can be tedious without a user-friendly interface. It shouldn’t be too
hard to beat exisiting online travel log services, such as the well-known Dutch
website &lt;a href=&quot;http://waarbenjij.nu&quot;&gt;‘WaarBenJij.nu’&lt;/a&gt;, in this aspect. Besides, what
happens to your data when the online travel log company goes bankrupt?  These
two shortcomings are easily addressed with common programmer tools like Emacs’
&lt;a href=&quot;http://orgmode.org&quot;&gt;Org mode&lt;/a&gt; and distributed version control systems like
&lt;a href=&quot;https://git-scm.com&quot;&gt;Git&lt;/a&gt;. While most programmers are familiar with the
features of Git and GitHub, those of Org mode are less-known:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Org mode is for keeping notes, maintaining TODO lists, planning projects, and
authoring documents with a fast and effective plain-text system.” —
&lt;a href=&quot;http://orgmode.org&quot;&gt;http://orgmode.org&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In addition to Org mode, we use the
&lt;a href=&quot;http://orgmode.org/worg/org-contrib/babel/&quot;&gt;Babel&lt;/a&gt; extension to execute source
code in various languages (in this case just Shell and Ruby) in between the blog
post paragraphs. By &lt;em&gt;using these blocks exclusively&lt;/em&gt;, we will create &lt;em&gt;all the
code necessary&lt;/em&gt; to: geocode the locations in the travel log to coordinates (and
install a library that helps us do this), convert the travel log to a GeoJSON
file, commit and push it into a new repository, and open GitHub in a browser at
the right URL. In fact, you can copy &amp;amp; paste this blog post into Emacs, enter
&lt;code class=&quot;highlighter-rouge&quot;&gt;org-mode&lt;/code&gt; and execute everything (&lt;code class=&quot;highlighter-rouge&quot;&gt;org-babel-execute-buffer&lt;/code&gt;) to reproduce my
steps.&lt;/p&gt;

&lt;p&gt;Enough talking, let’s build this thing! We begin by defining the travel log, and
‘store it in a variable’ called &lt;code class=&quot;highlighter-rouge&quot;&gt;travel-log&lt;/code&gt;.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-cucumber&quot; data-lang=&quot;cucumber&quot;&gt;&lt;span class=&quot;c&quot;&gt;#+NAME: travel-log&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+RESULTS:&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Date&lt;/span&gt;             &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Location&lt;/span&gt;                  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;------------------+---------------------------&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;&amp;lt;2015-08-10&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Mon&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Utrecht,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;The&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Netherlands&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;&amp;lt;2015-08-10&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Mon&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Kožná,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Prague&lt;/span&gt;             &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;&amp;lt;2015-08-12&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Wed&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Prenzlauer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Berg,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Berlin&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;&amp;lt;2015-08-13&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Thu&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;A&amp;amp;O&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Hamburg&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;City,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Hamburg&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;&amp;lt;2015-08-14&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Fri&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Utrecht,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;The&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Netherlands&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The brackets (&lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;gt;&lt;/code&gt;) around the dates indicate an Org mode timestamp. We
can easily add and manipulate dates by using the datepicker (&lt;code class=&quot;highlighter-rouge&quot;&gt;C-c .&lt;/code&gt;, which, for
those unfamiliar with Emacs, means pressing the Control and C keys
simultaneously before hitting the dot) and use &lt;code class=&quot;highlighter-rouge&quot;&gt;TAB&lt;/code&gt; to move through the table:
a user-friendly interface.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/emacs_org_mode_datepicker.png&quot; alt=&quot;Emacs Org mode datepicker&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In order to geocode the location names to coordinates we will use the
&lt;a href=&quot;https://github.com/alexreisner/geocoder&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;geocoder&lt;/code&gt; Ruby gem&lt;/a&gt;. A shell source
code block is an excellent way to install it, most importantly because the
output displays the version that I used while writing the blog post, which
improves reproducibility.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;c&quot;&gt;#+HEADER: :results output&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+BEGIN_SRC sh&lt;/span&gt;

gem &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;geocoder

&lt;span class=&quot;c&quot;&gt;#+END_SRC&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#+RESULTS:&lt;/span&gt;
: Successfully installed geocoder-1.2.11
: Parsing documentation &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;geocoder-1.2.11
: Done installing documentation &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;geocoder after 1 seconds
: 1 gem installed&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now we will geocode the locations using the gem above. We don’t want to get
rate-limited by the Google Maps API, so that’s why we create the
&lt;code class=&quot;highlighter-rouge&quot;&gt;geolocation-cache&lt;/code&gt; table with the distinct locations and their coordinates. For
instance, Utrecht is listed twice in the travel log but only geocoded once. By
the way, the &lt;code class=&quot;highlighter-rouge&quot;&gt;#+HEADER:&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;#+BEGIN_SRC&lt;/code&gt; lines are instructions to Babel. I
included them to enhance reproducibility.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;#+HEADER: :var travel_log=travel-log&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+BEGIN_SRC ruby&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;locations&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;travel_log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# We only need the second column: the location&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;distinct_locations&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;locations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;uniq&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'geocoder'&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;distinct_locations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;geo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Geocoder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;coordinates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;geo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;geometry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'location'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;coordinates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'lng'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;coordinates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'lat'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+END_SRC&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;#+NAME: geolocation-cache&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+RESULTS:&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Utrecht&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;The&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Netherlands&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;  &lt;span class=&quot;mf&quot;&gt;5.1214201&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;52.09073739999999&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Ko&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;ž&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;á&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Prague&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;14.4213456&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;        &lt;span class=&quot;mf&quot;&gt;50.0862754&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Prenzlauer&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Berg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Berlin&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;mf&quot;&gt;13.44009&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;          &lt;span class=&quot;mf&quot;&gt;52.54114&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;O&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hamburg&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;City&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hamburg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;  &lt;span class=&quot;mf&quot;&gt;9.9936818&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;        &lt;span class=&quot;mf&quot;&gt;53.5510846&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Before we move on to the GeoJSON conversion, we have to specify a path where we
can save the file.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;c&quot;&gt;#+NAME: geojson-file-path&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+RESULTS:&lt;/span&gt;
: /tmp/example_travel_log/my_trip.geojson&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The following source block joins the &lt;code class=&quot;highlighter-rouge&quot;&gt;travel-log&lt;/code&gt; with the &lt;code class=&quot;highlighter-rouge&quot;&gt;geolocation-cache&lt;/code&gt;,
builds a &lt;a href=&quot;http://geojson.org&quot;&gt;GeoJSON-formatted&lt;/a&gt; structure and saves it to the
&lt;code class=&quot;highlighter-rouge&quot;&gt;geojson-file-path&lt;/code&gt;.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;#+HEADER: :results silent&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+HEADER: :var geojson_file_path=geojson-file-path&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+HEADER: :var geolocation_cache=geolocation-cache&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+HEADER: :var travel_log=travel-log&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+BEGIN_SRC ruby&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;coordinates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;geolocation_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;longitude&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;latitude&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;longitude&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;latitude&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'date'&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;geojson_features&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# We use each_cons so we can draw lines between the locations&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;travel_log&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;travel_log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_cons&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next_entry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;org_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org_date&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;geojson_features&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Feature'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;geometry: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Point'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;coordinates: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;coordinates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;properties: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;date&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;next&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next_entry&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next_loc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next_entry&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;geojson_features&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Feature'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;geometry: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'LineString'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;coordinates: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;coordinates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;coordinates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;next_loc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'fileutils'&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;repository_dir&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;geojson_file_path&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;FileUtils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mkdir_p&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;repository_dir&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'json'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;geojson_file_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'w'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pretty_generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'FeatureCollection'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;features: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;geojson_features&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#+END_SRC&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Let’s verify the contents of the newly created file before we move on.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;c&quot;&gt;#+HEADER: :results output&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+HEADER: :var GEOJSON_FILE_PATH=geojson-file-path&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+BEGIN_SRC sh&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 33 &lt;span class=&quot;s1&quot;&gt;''&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$GEOJSON_FILE_PATH&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo &lt;/span&gt;etc...
&lt;span class=&quot;c&quot;&gt;#+END_SRC&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#+RESULTS:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+begin_example&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;==&amp;gt;&lt;/span&gt; /tmp/example_travel_log/my_trip.geojson &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;FeatureCollection&quot;&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;features&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;Feature&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;geometry&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;Point&quot;&lt;/span&gt;,
        &lt;span class=&quot;s2&quot;&gt;&quot;coordinates&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;
          5.1214201,
          52.09073739999999
        &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;properties&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Location&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;Utrecht, The Netherlands&quot;&lt;/span&gt;,
        &lt;span class=&quot;s2&quot;&gt;&quot;Date&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;2015-08-10&quot;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;,
    &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;Feature&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;geometry&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;LineString&quot;&lt;/span&gt;,
        &lt;span class=&quot;s2&quot;&gt;&quot;coordinates&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;
            5.1214201,
            52.09073739999999
          &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;,
          &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;
            14.4213456,
            50.0862754
          &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;,
etc...
&lt;span class=&quot;c&quot;&gt;#+end_example&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The date, location and coordinates seem to match with the travel log we
specified earlier. Let’s create a repository, commit, push and open GitHub to
check it out!&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;c&quot;&gt;#+HEADER: :results output silent&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+HEADER: :var GEOJSON_FILE_PATH=geojson-file-path&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+BEGIN_SRC sh&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$GEOJSON_FILE_PATH&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;FILENAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$GEOJSON_FILE_PATH&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;hub
hub init
hub create
hub add &lt;span class=&quot;nv&quot;&gt;$FILENAME&lt;/span&gt;
hub commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Update travel_log'&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$FILENAME&lt;/span&gt;
hub push origin master

&lt;span class=&quot;nv&quot;&gt;GH_PATH_ROOT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;hub remote &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;fetch | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-oE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\w+\/\w+'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
hub browse &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$GH_PATH_ROOT&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/blob/master/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FILENAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#+END_SRC&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;*Safari opens…*&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/pepijn/example_travel_log/blob/master/my_trip.geojson&quot;&gt;&lt;img src=&quot;/images/example_travel_log_github.png&quot; alt=&quot;My travel log example&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works; all the destinations from the travel log show up on the map with lines
connecting them. Whenever we change the travel log or the code we can just rerun
all the source code blocks and check out the new result; no slow switches
between editors and command lines. All in all, the consequential higher speed of
development and improved transparency of the program will most definitely make
me pick Org mode and Babel for future projects.&lt;/p&gt;

&lt;p&gt;For a more feature complete implementation that might actually be useful for
real-life travel logging, please check out:
&lt;a href=&quot;https://github.com/pepijn/travel_log&quot;&gt;https://github.com/pepijn/travel_log&lt;/a&gt;. Manipulating
it (adding a destination) looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/pepijn/travel_log&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/pepijn/travel_log/d441172af826a6de697286222ca9158c76546559/media/demo.gif&quot; alt=&quot;manipulating my feature complete travel log&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy hacking!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://twitter.com/share&quot; class=&quot;twitter-share-button&quot; data-via=&quot;ppnlo&quot;&gt;Tweet&lt;/a&gt; &lt;script&gt;!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Sun, 04 Oct 2015 00:00:00 -0300</pubDate>
        <link>https://p.epij.nl/emacs/org-mode/git/geojson/2015/10/04/building-a-geojson-travel-log-an-introduction-to-org-mode-and-babel/</link>
        <guid isPermaLink="true">https://p.epij.nl/emacs/org-mode/git/geojson/2015/10/04/building-a-geojson-travel-log-an-introduction-to-org-mode-and-babel/</guid>
      </item>
    
  </channel>
</rss>
